Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions packages/assets/src/scss/_overflow-list.scss
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
.ids-overflow-list {
overflow: hidden;
display: flex;
flex-wrap: nowrap;

& > * {
flex-shrink: 0;
&__items {
overflow: hidden;
display: flex;
flex-wrap: nowrap;

& > * {
flex-shrink: 0;
}
}
}
11 changes: 11 additions & 0 deletions packages/assets/src/scss/inputs/_dropdown.scss
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
}

&__selection-info {
flex-grow: 1;
user-select: none;
}

Expand Down Expand Up @@ -122,6 +123,16 @@
color: $color-primary-80;
}
}

&__selection-info {
.ids-overflow-list {
margin: calculateRem(-4px);

.ids-chip {
margin: calculateRem(4px);
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export const ManyItemsManySelected: Story = {
name: 'Many Items / Many Selected',
args: {
items: generateItemsArray(MANY_ITEMS_LENGTH),
value: ['3', '4', '8'],
value: ['3', '4', '8', '11'],
},
parameters: {
wrapperHeight: WRAPPER_HEIGHT_FOR_LONG_LIST,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ export const Default: Story = {
await userEvent.click(lastItem);
await userEvent.click(canvasElement);

const selectedInfo = canvas.getByText('Item 1, Item 3', { selector: 'div' });
const selectedItem = canvas.getByText('Item 1', { selector: 'div' });
const overflowItem = canvas.getByText('+1', { selector: 'div' });

await expect(selectedInfo).toBeVisible();
await expect(selectedItem).toBeVisible();
await expect(overflowItem).toBeVisible();
await expect(() => {
canvas.getByText('Item 2', { selector: 'div' });
}).toThrowError();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import React from 'react';
import { ExtraParamsType, getNextFocusableItem } from '../utils/focus';
import { BaseDropdown } from '@ids-partials/BaseDropdown';
import { CheckboxInput } from '@ids-components/Checkbox';
import { Chip } from '@ids-components/Chip';
import { OverflowList } from '@ids-components/OverflowList';
import { createCssClassNames } from '@ids-core';
import { withStateValue } from '@ids-hoc/withStateValue';

Expand Down Expand Up @@ -47,8 +49,20 @@ export const DropdownMultiInput = ({
};
const selectedItems = value.length ? items.filter((item) => value.includes(item.id)) : [];
const renderSelectedItems = () => (
<>{selectedItems.map((item) => item.label).join(', ')}</>
// TODO: replace with chips when done
<OverflowList
items={selectedItems}
renderItem={(item) => (
<Chip
key={item.id}
onDelete={() => {
changeValue(item.id);
}}
>
{item.label}
</Chip>
)}
renderMore={({ hiddenCount }) => <Chip isDeletable={false}>+{hiddenCount}</Chip>}
/>
);
const renderSource = () => {
return (
Expand Down
33 changes: 25 additions & 8 deletions packages/components/src/components/OverflowList/OverflowList.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';

import { createCssClassNames } from '@ids-core/helpers/cssClassNames';
import { useDebounce } from '@ids-hooks/useDebounce';

import { OverflowListCalculateAction as Actions, OverflowListProps } from './OverflowList.types';

const RESIZE_TIMEOUT = 200;

export const OverflowList = <ItemProps extends { id: string }>({
className = '',
items = [],
renderItem,
renderMore,
}: OverflowListProps<ItemProps>) => {
const listRef = useRef<HTMLDivElement | null>(null);
const itemsRef = useRef<HTMLDivElement | null>(null);
const [itemsWidth, setItemsWidth] = useState(0);
const [currentAction, setCurrentAction] = useState(Actions.None);
const [numberOfVisibleItems, setNumberOfVisibleItems] = useState(items.length);
const debounce = useDebounce(RESIZE_TIMEOUT);
const componentClassName = createCssClassNames({
'ids-overflow-list': true,
[className]: !!className,
});
const recalculateVisibleItems = () => {
if (!listRef.current) {
if (!itemsRef.current) {
return;
}

const itemsNodes = Array.from(listRef.current.children);
const { right: listRightPosition } = listRef.current.getBoundingClientRect();
const itemsNodes = Array.from(itemsRef.current.children);
const { right: listRightPosition } = itemsRef.current.getBoundingClientRect();
const newNumberOfVisibleItems = itemsNodes.findIndex((itemNode) => {
const { right: itemRightPosition } = itemNode.getBoundingClientRect();

Expand All @@ -45,10 +51,13 @@ export const OverflowList = <ItemProps extends { id: string }>({
const listResizeObserver = useMemo(
() =>
new ResizeObserver(() => {
setNumberOfVisibleItems(items.length);
setCurrentAction(Actions.CalculateItems);
debounce(() => {
setItemsWidth(listRef.current?.offsetWidth ?? 0);
setNumberOfVisibleItems(items.length);
setCurrentAction(Actions.CalculateItems);
});
}),
[items.length],
[items.length, debounce],
);
const renderItems = () => {
return items.slice(0, numberOfVisibleItems).map((item) => renderItem(item));
Expand All @@ -71,6 +80,12 @@ export const OverflowList = <ItemProps extends { id: string }>({
}
}, [currentAction, numberOfVisibleItems]);

useLayoutEffect(() => {
if (listRef.current) {
setItemsWidth(listRef.current.offsetWidth);
}
}, []);

useEffect(() => {
if (currentAction === Actions.None) {
setNumberOfVisibleItems(items.length);
Expand All @@ -90,8 +105,10 @@ export const OverflowList = <ItemProps extends { id: string }>({

return (
<div className={componentClassName} ref={listRef}>
{renderItems()}
{renderOverflow()}
<div className="ids-overflow-list__items" ref={itemsRef} style={{ width: `${itemsWidth}px` }}>
{renderItems()}
{renderOverflow()}
</div>
</div>
);
};
26 changes: 26 additions & 0 deletions packages/components/src/hooks/useDebounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useCallback, useEffect, useRef } from 'react';

export const useDebounce = (delay: number) => {
const timeoutRef = useRef<NodeJS.Timeout | null>(null);

useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
});

return useCallback(
(callback: () => void) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}

timeoutRef.current = setTimeout(() => {
callback();
}, delay);
},
[delay],
);
};