From 29a22d5c4aca7d4d851c18cce59e06aca3d3c859 Mon Sep 17 00:00:00 2001 From: PeterShershov Date: Wed, 18 Dec 2024 16:50:39 +0200 Subject: [PATCH] refactor list component to improve range selection logic --- packages/components/src/list/list.tsx | 96 ++++++++++++++++++--------- 1 file changed, 66 insertions(+), 30 deletions(-) diff --git a/packages/components/src/list/list.tsx b/packages/components/src/list/list.tsx index 9ca3e135..c8d157c2 100644 --- a/packages/components/src/list/list.tsx +++ b/packages/components/src/list/list.tsx @@ -97,15 +97,15 @@ export function List({ } }, [selectedIds]); - const indexMap = useRef(new Map()); + const { current: indexMap } = useRef(new Map()); const itemsToRender = useMemo(() => { - indexMap.current.clear(); + indexMap.clear(); const jsxElements: JSX.Element[] = []; for (const [index, item] of items.entries()) { const id = getId(item); - indexMap.current.set(id, index); + indexMap.set(id, index); jsxElements.push( ({ } return jsxElements; - }, [items, getId, ItemRenderer, onItemMount, onItemUnmount, setFocusedId, focusedId, selectedIds, setSelectedIds]); + }, [ + indexMap, + items, + getId, + ItemRenderer, + onItemMount, + onItemUnmount, + setFocusedId, + focusedId, + selectedIds, + setSelectedIds, + ]); const onClick = useIdListener( useCallback( @@ -138,10 +149,10 @@ export function List({ setFocusedId(id); - const isSameSelected = selectedIds.includes(id); + const isAlreadySelected = selectedIds.includes(id); if (!enableMultiselect) { - if (isSameSelected) { + if (isAlreadySelected) { return; } @@ -152,38 +163,26 @@ export function List({ const isCtrlPressed = ev.ctrlKey || ev.metaKey; const isShiftPressed = ev.shiftKey; - if (isCtrlPressed && isSameSelected) { + if (isCtrlPressed && isAlreadySelected) { setSelectedIds(selectedIds.filter((selectedId) => selectedId !== id)); } else if (isCtrlPressed) { setSelectedIds([...selectedIds, id]); } else if (isShiftPressed) { - const [first] = selectedIds; - - if (!first) { - setSelectedIds([id]); - return; - } - - // if the `rangeSelectionAnchorId` is not set, we will consider the - // first selected item as the starting point of the range selection. - const firstIndex = indexMap.current.get(rangeSelectionAnchorId.current || first); - const selectedIndex = indexMap.current.get(id); - - if (firstIndex === undefined || selectedIndex === undefined) { - setSelectedIds([id]); - return; - } - - const startIndex = Math.min(firstIndex, selectedIndex); - const endIndex = Math.max(firstIndex, selectedIndex); - - // we add 1 to `endIndex` to include the last item in the selection - setSelectedIds(items.slice(startIndex, endIndex + 1).map(getId)); + setSelectedIds( + getRangeSelection({ + items, + id, + indexMap, + selectedIds, + rangeSelectionAnchorId: rangeSelectionAnchorId.current, + getId, + }), + ); } else { setSelectedIds([id]); } }, - [enableMultiselect, getId, setSelectedIds, items, rangeSelectionAnchorId, selectedIds, setFocusedId], + [setFocusedId, selectedIds, enableMultiselect, setSelectedIds, items, indexMap, getId], ), ); @@ -234,3 +233,40 @@ function ItemRendererWrapped({ return ; } + +function getRangeSelection({ + id, + indexMap, + items, + selectedIds, + rangeSelectionAnchorId, + getId, +}: { + selectedIds: string[]; + id: string; + items: T[]; + indexMap: Map; + rangeSelectionAnchorId?: string; + getId: (item: T) => string; +}) { + const [first] = selectedIds; + + if (!first) { + return [id]; + } + + // if the `rangeSelectionAnchorId` is not set, we will consider the + // first selected item as the starting point of the range selection. + const firstIndex = indexMap.get(rangeSelectionAnchorId || first); + const selectedIndex = indexMap.get(id); + + if (firstIndex === undefined || selectedIndex === undefined) { + return [id]; + } + + const startIndex = Math.min(firstIndex, selectedIndex); + const endIndex = Math.max(firstIndex, selectedIndex); + + // we add 1 to `endIndex` to include the last item in the selection + return items.slice(startIndex, endIndex + 1).map(getId); +}