diff --git a/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableList.tsx b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableList.tsx
index fa1586fc50..925078e712 100644
--- a/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableList.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableList.tsx
@@ -7,12 +7,13 @@ import {
useState,
} from 'react';
+import { ExpandableListResizeEffect } from '@/ui/layout/expandable-list/components/ExpandableListResizeEffect';
import { ExpandedListDropdown } from '@/ui/layout/expandable-list/components/ExpandedListDropdown';
import { isFirstOverflowingChildElement } from '@/ui/layout/expandable-list/utils/isFirstOverflowingChildElement';
import { isDefined } from 'twenty-shared/utils';
import { ChipSize } from 'twenty-ui/data-display';
-import { OverflowingTextWithTooltip } from 'twenty-ui/surfaces';
import { AnimatedContainer } from 'twenty-ui/layout';
+import { OverflowingTextWithTooltip } from 'twenty-ui/surfaces';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledContainer = styled.div`
@@ -94,6 +95,10 @@ export const ExpandableList = ({
const hiddenChildrenCount = children.length - firstHiddenChildIndex;
const canDisplayChipCount = isChipCountDisplayed && hiddenChildrenCount > 0;
+ const visibleChildren = isChipCountDisplayed
+ ? children.slice(0, firstHiddenChildIndex)
+ : children;
+
const handleChipCountClick = useCallback((event: React.MouseEvent) => {
event.stopPropagation();
setIsListExpanded(true);
@@ -103,9 +108,6 @@ export const ExpandableList = ({
setFirstHiddenChildIndex(children.length);
}, [children.length]);
- // Recompute first hidden child when:
- // - isChipCountDisplayed changes
- // - children length changes
useEffect(() => {
resetFirstHiddenChildIndex();
}, [isChipCountDisplayed, children.length, resetFirstHiddenChildIndex]);
@@ -137,22 +139,31 @@ export const ExpandableList = ({
: () => setIsChipCountDisplayedInternal(false)
}
>
+ {isChipCountDisplayed && (
+
+ )}
- {children.slice(0, firstHiddenChildIndex).map((child, index) => (
+ {visibleChildren.map((child, index) => (
{
- if (
- // First element is always displayed.
- index > 0 &&
- isFirstOverflowingChildElement({
- containerElement: childrenContainerElement,
- childElement,
- })
- ) {
- setFirstHiddenChildIndex(index);
- }
- }}
+ ref={
+ isChipCountDisplayed
+ ? (childElement) => {
+ if (
+ index > 0 &&
+ isFirstOverflowingChildElement({
+ containerElement: childrenContainerElement,
+ childElement,
+ })
+ ) {
+ setFirstHiddenChildIndex(index);
+ }
+ }
+ : undefined
+ }
>
{child}
diff --git a/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableListResizeEffect.tsx b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableListResizeEffect.tsx
new file mode 100644
index 0000000000..8d1cce8f36
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableListResizeEffect.tsx
@@ -0,0 +1,57 @@
+import { type RefObject, useEffect } from 'react';
+import { isDefined } from 'twenty-shared/utils';
+
+const RESIZE_THRESHOLD_PX = 1;
+
+const RESIZE_SETTLE_DELAY_MS = 100;
+
+type ExpandableListResizeEffectProps = {
+ containerRef: RefObject;
+ onContainerWidthChange: () => void;
+};
+
+export const ExpandableListResizeEffect = ({
+ containerRef,
+ onContainerWidthChange,
+}: ExpandableListResizeEffectProps) => {
+ useEffect(() => {
+ const containerElement = containerRef.current;
+
+ if (!isDefined(containerElement)) {
+ return;
+ }
+
+ let previousWidth = containerElement.clientWidth;
+ let settleTimeoutId: ReturnType | undefined;
+
+ const resizeObserver = new ResizeObserver((entries) => {
+ const entry = entries[0];
+
+ if (!isDefined(entry)) {
+ return;
+ }
+
+ const newWidth = entry.contentRect.width;
+
+ if (Math.abs(newWidth - previousWidth) <= RESIZE_THRESHOLD_PX) {
+ return;
+ }
+
+ previousWidth = newWidth;
+ clearTimeout(settleTimeoutId);
+ settleTimeoutId = setTimeout(
+ onContainerWidthChange,
+ RESIZE_SETTLE_DELAY_MS,
+ );
+ });
+
+ resizeObserver.observe(containerElement);
+
+ return () => {
+ clearTimeout(settleTimeoutId);
+ resizeObserver.disconnect();
+ };
+ }, [containerRef, onContainerWidthChange]);
+
+ return null;
+};
diff --git a/packages/twenty-front/src/modules/ui/layout/expandable-list/components/__stories__/ExpandableList.stories.tsx b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/__stories__/ExpandableList.stories.tsx
index 495656523b..007f82ccaa 100644
--- a/packages/twenty-front/src/modules/ui/layout/expandable-list/components/__stories__/ExpandableList.stories.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/__stories__/ExpandableList.stories.tsx
@@ -1,7 +1,8 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
-import { expect, userEvent, within } from 'storybook/test';
+import { expect, userEvent, waitFor, within } from 'storybook/test';
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
+import { isDefined } from 'twenty-shared/utils';
import { Tag } from 'twenty-ui/data-display';
import { ComponentDecorator } from 'twenty-ui/testing';
import { MAIN_COLOR_NAMES } from 'twenty-ui/theme';
@@ -56,3 +57,69 @@ export const WithExpandedList: Story = {
expect(await bodyCanvas.findByText('Option 7')).toBeDefined();
},
};
+
+const OPTIONS_COUNT = 7;
+const COLLAPSED_WIDTH_PX = 96;
+
+const optionTags = Array.from({ length: OPTIONS_COUNT }, (_, index) => (
+
+));
+
+const countRenderedOptions = (canvas: ReturnType) =>
+ canvas.queryAllByText(/^Option \d+$/).length;
+
+export const RecomputesVisibleChipsOnResize: Story = {
+ render: () => (
+
+ {optionTags}
+
+ ),
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const resizableCell = canvasElement.querySelector(
+ '[data-resizable-cell]',
+ );
+
+ await waitFor(() => {
+ expect(countRenderedOptions(canvas)).toBeLessThan(OPTIONS_COUNT);
+ });
+
+ const collapsedCount = countRenderedOptions(canvas);
+
+ if (isDefined(resizableCell)) {
+ resizableCell.style.width = '100%';
+ }
+
+ await waitFor(() => {
+ expect(countRenderedOptions(canvas)).toBeGreaterThan(collapsedCount);
+ });
+ },
+};
+
+export const ShowsAllChipsWhenCountHidden: Story = {
+ render: () => (
+
+ {optionTags}
+
+ ),
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ await waitFor(() => {
+ expect(countRenderedOptions(canvas)).toBe(OPTIONS_COUNT);
+ });
+ },
+};