Skip to content
Draft
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@internxt/ui",
"version": "0.1.29",
"version": "0.1.30",
"description": "Library of Internxt components",
"repository": {
"type": "git",
Expand Down
24 changes: 16 additions & 8 deletions src/components/data-display/list/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface ListProps<T, F> {
onMouseEnter?: () => void;
onMouseLeave?: () => void;
headerBackgroundColor?: string;
getItemId?: (item: T) => string | number;
keyBoardShortcutActions?: {
onShiftFKeysPressed?: () => void;
onRKeyPressed?: () => void;
Expand Down Expand Up @@ -121,6 +122,10 @@ export interface ListProps<T, F> {
* @property {string} [headerBackgroundColor]
* - Optional background color for the header.
*
* @property {(item: T) => string | number} [getItemId]
* - Optional function returning the unique identity of an item (used for selection, context menu and React keys).
* Defaults to `item.id`.
*
* @property {
* { onShiftFKeysPressed?: () => void; onRKeyPressed?: () => void; onBackspaceKeyPressed?: () => void }
* } [keyBoardShortcutActions]
Expand All @@ -130,7 +135,7 @@ export interface ListProps<T, F> {
* - The rendered list component.
*/

const List = <T extends { id: number }, F extends keyof T>({
const List = <T extends { id?: number | string }, F extends keyof T>({
header,
checkboxDataCy,
items,
Expand All @@ -157,15 +162,18 @@ const List = <T extends { id: number }, F extends keyof T>({
headerBackgroundColor = 'bg-surface',
keyBoardShortcutActions,
disableKeyboardShortcuts,
getItemId,
}: ListProps<T, F>): JSX.Element => {
const containerRef = useRef<HTMLDivElement>(null);
const getKey = getItemId ?? ((item: T) => item.id as string | number);
const isItemSelected = (item: T) => {
return selectedItems.some((i) => item.id === i.id);
const key = getKey(item);
return selectedItems.some((i) => getKey(i) === key);
};
const container = document.getElementById('scrollableList');
const isVerticalScrollbarVisible = container && container.scrollHeight > container.clientHeight;
const isEmptyState = !hasMoreItems && items.length === 0 && !isLoading;
const [idItemContextMenuOpen, setIdItemContextMenuOpen] = useState<number | null>(null);
const [idItemContextMenuOpen, setIdItemContextMenuOpen] = useState<string | number | null>(null);

const skeletonData = new Array(25).fill(0).map(() => ({
skeletonItem: skinSkeleton,
Expand Down Expand Up @@ -213,7 +221,7 @@ const List = <T extends { id: number }, F extends keyof T>({
};

const selectAllItems = () => {
const notSelectedItems = items.filter((item) => !selectedItems.some((s) => s.id === item.id));
const notSelectedItems = items.filter((item) => !selectedItems.some((s) => getKey(s) === getKey(item)));
const changesToMake = notSelectedItems.map((item) => ({ props: item, value: true }));
onSelectedItemsChanged(changesToMake);
};
Expand Down Expand Up @@ -278,7 +286,7 @@ const List = <T extends { id: number }, F extends keyof T>({
if (!isItemSelected(props)) {
unselectAllItemsAndSelectOne(props);
}
setIdItemContextMenuOpen(props.id);
setIdItemContextMenuOpen(getKey(props));
};

const onCloseContextMenu = () => {
Expand All @@ -288,7 +296,7 @@ const List = <T extends { id: number }, F extends keyof T>({
const handleThreeDotsButtonClick = (e: React.MouseEvent<HTMLButtonElement>, item: T) => {
e.stopPropagation();

setIdItemContextMenuOpen((prevId) => (prevId ? null : item.id));
setIdItemContextMenuOpen((prevId) => (prevId !== null ? null : getKey(item)));
if (!isItemSelected(item)) {
unselectAllItemsAndSelectOne(item);
}
Expand Down Expand Up @@ -330,10 +338,10 @@ const List = <T extends { id: number }, F extends keyof T>({
>
{items.map((item, index) => (
<ListItem<T>
key={item.id}
key={getKey(item)}
item={item}
listIndex={index}
isOpen={idItemContextMenuOpen === item.id}
isOpen={idItemContextMenuOpen === getKey(item)}
onClose={onCloseContextMenu}
itemComposition={itemComposition}
selected={isItemSelected(item)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/data-display/list/ListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ interface ListItemProps<T> {
}
const TOP_MIN_HEIGHT = 500;

const ListItem = <T extends { id: number }>({
const ListItem = <T extends { id?: number | string }>({
item,
listIndex,
itemComposition,
Expand Down
58 changes: 58 additions & 0 deletions src/components/data-display/list/__test__/List.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -348,3 +348,61 @@ describe('List component', () => {
expect(mockOnEnterPressed).toHaveBeenCalledWith(items[0]);
});
});

describe('List component with getItemId', () => {
type UuidItem = { id?: number; uuid: string; name: string; age: number };

const uuidHeader: HeaderProps<UuidItem, keyof UuidItem>[] = [
{ label: 'Name', width: '100px', name: 'name', orderable: true, defaultDirection: 'ASC' },
];
const uuidItems: UuidItem[] = [
{ id: 0, uuid: 'uuid-1', name: 'File 1', age: 25 },
{ id: 0, uuid: 'uuid-2', name: 'File 2', age: 30 },
{ uuid: 'uuid-3', name: 'File 3', age: 35 },
];

const renderUuidList = (selected: UuidItem[]) =>
render(
<List<UuidItem, keyof UuidItem>
header={uuidHeader}
items={uuidItems}
selectedItems={selected}
itemComposition={[(props) => <div>{props.name}</div>]}
menu={menu}
onSelectedItemsChanged={mockOnSelectedItemsChanged}
getItemId={(item) => item.uuid}
/>,
);

afterEach(() => {
vi.clearAllMocks();
});

it('should select all not selected items by uuid when ids collide or are missing', () => {
const { container } = renderUuidList([uuidItems[0]]);

fireEvent.keyDown(container, { key: 'a', ctrlKey: true });

expect(mockOnSelectedItemsChanged).toHaveBeenCalledWith([
{ props: uuidItems[1], value: true },
{ props: uuidItems[2], value: true },
]);
});

it('should toggle selection by uuid when Ctrl key is pressed', () => {
const { getByText } = renderUuidList([uuidItems[0]]);

fireEvent.click(getByText('File 2'), { ctrlKey: true });

expect(mockOnSelectedItemsChanged).toHaveBeenCalledWith([{ props: uuidItems[1], value: true }]);
});

it('should open context menu only for the right clicked item', () => {
const { getByText, getAllByText } = renderUuidList([uuidItems[1]]);

fireEvent.contextMenu(getByText('File 2'));

expect(mockOnSelectedItemsChanged).not.toHaveBeenCalled();
expect(getAllByText('Option 1')).toHaveLength(1);
});
});
Loading