From 4a0b4b6ebf703fea201933d17b23faa957ce032a Mon Sep 17 00:00:00 2001 From: daisy <47104575+linxianxi@users.noreply.github.com> Date: Sun, 3 Sep 2023 19:53:03 +0800 Subject: [PATCH] fix: fixed some boundary issues with disabled (#12) --- docs/examples/sort.tsx | 35 +++++++++++++-- docs/examples/virtual-list.tsx | 50 +++++++++++----------- docs/guides/sort.md | 2 +- docs/guides/sort.zh-CN.md | 2 +- docs/guides/virtual-list.md | 2 +- docs/guides/virtual-list.zh-CN.md | 2 +- package.json | 2 +- pnpm-lock.yaml | 22 ++++------ src/Selectable.tsx | 71 +++++++++++++++++-------------- src/hooks/useLatest.ts | 8 ++++ 10 files changed, 115 insertions(+), 81 deletions(-) create mode 100644 src/hooks/useLatest.ts diff --git a/docs/examples/sort.tsx b/docs/examples/sort.tsx index e4e915b..6139e6d 100644 --- a/docs/examples/sort.tsx +++ b/docs/examples/sort.tsx @@ -1,21 +1,23 @@ -import { DndContext } from '@dnd-kit/core'; +import { DndContext, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; import { SortableContext, useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import React, { useState } from 'react'; import Selectable, { useSelectable } from 'react-selectable-box'; const list: string[] = []; -for (let i = 0; i < 25; i++) { +for (let i = 0; i < 200; i++) { list.push(String(i)); } const Item = ({ value, disabled, + onClick, selectedLength, }: { value: React.Key; disabled: boolean; + onClick: (isSelected: boolean) => void; selectedLength: number; }) => { const { @@ -54,16 +56,18 @@ const Item = ({ justifyContent: 'center', alignItems: 'center', position: 'relative', - zIndex: isDragging ? 999 : undefined, width: 50, height: 50, borderRadius: 4, transform: CSS.Transform.toString(transform), transition, + zIndex: isDragging ? 999 : undefined, + cursor: isSelected ? 'move' : undefined, visibility: isSelected && !isDragging && isSorting ? 'hidden' : undefined, border: isAdding ? '1px solid #1677ff' : undefined, background: isRemoving ? 'red' : isSelected ? '#1677ff' : '#ccc', }} + onClick={() => onClick(isSelected)} > {value} {isDragging && ( @@ -79,8 +83,18 @@ export default () => { const [beforeSortItems, setBeforeSortItems] = useState([]); const [isSorting, setIsSorting] = useState(false); + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + // make item click event available + distance: 1, + }, + }), + ); + return ( { setIsSorting(true); const index = event.active.data.current?.sortable.index as number; @@ -121,6 +135,7 @@ export default () => { disabled={isSorting} getContainer={() => document.querySelector('.container') as HTMLElement} onEnd={(selectingValue, { added, removed }) => { + console.log('onEnd'); const result = value.concat(added).filter((i) => !removed.includes(i)); setValue(result); }} @@ -137,7 +152,19 @@ export default () => { className="container" > {items.map((i) => ( - + { + if (isSelected) { + setValue(value.filter((val) => val !== i)); + } else { + setValue(value.concat(i)); + } + }} + /> ))} diff --git a/docs/examples/virtual-list.tsx b/docs/examples/virtual-list.tsx index bf9e56e..2d3f394 100644 --- a/docs/examples/virtual-list.tsx +++ b/docs/examples/virtual-list.tsx @@ -1,16 +1,14 @@ -import React, { useState } from 'react'; +import React, { HTMLAttributes, useState } from 'react'; import Selectable, { useSelectable } from 'react-selectable-box'; -import { FixedSizeList } from 'react-window'; +import { VirtuosoGrid } from 'react-virtuoso'; const list: string[] = []; -for (let i = 0; i < 2000; i++) { +for (let i = 0; i < 2002; i++) { list.push(String(i)); } -const columnCount = 10; - -const Cell = ({ value, style }: { value: string; style: React.CSSProperties }) => { - const { setNodeRef, isSelected, isAdding, isRemoving, isDragging, isSelecting } = useSelectable({ +const Item = ({ value }: { value: string }) => { + const { setNodeRef, isSelected, isAdding, isRemoving } = useSelectable({ value, }); @@ -18,28 +16,28 @@ const Cell = ({ value, style }: { value: string; style: React.CSSProperties }) =
+ > + {value} +
); }; -const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => { - const rowList = list.slice(index * columnCount, (index + 1) * columnCount); +const List: React.ForwardRefExoticComponent< + HTMLAttributes & React.RefAttributes +> = React.forwardRef(({ style, ...props }, ref) => { return ( -
- {rowList.map((i, idx) => { - const marginRight = idx < 9 ? 20 : 0; - return ; - })} -
+
); -}; +}); export default () => { const [value, setValue] = useState([]); @@ -54,15 +52,15 @@ export default () => { setValue(result); }} > - - {({ index, style }) => } - + itemContent={(index) => } + /> ); }; diff --git a/docs/guides/sort.md b/docs/guides/sort.md index 5271ec3..83b632a 100644 --- a/docs/guides/sort.md +++ b/docs/guides/sort.md @@ -6,6 +6,6 @@ group: order: 8 --- -### sort +### Use `dnd-kit` to sort after selecting some options diff --git a/docs/guides/sort.zh-CN.md b/docs/guides/sort.zh-CN.md index 83b632a..c0b9f39 100644 --- a/docs/guides/sort.zh-CN.md +++ b/docs/guides/sort.zh-CN.md @@ -6,6 +6,6 @@ group: order: 8 --- -### Use `dnd-kit` to sort after selecting some options +### 配合 `dnd-kit` 选择一些选项后进行排序 diff --git a/docs/guides/virtual-list.md b/docs/guides/virtual-list.md index 575fd85..e8fdfa2 100644 --- a/docs/guides/virtual-list.md +++ b/docs/guides/virtual-list.md @@ -6,6 +6,6 @@ group: order: 7 --- -### Box selection in a virtual list using `react-window` +### Box selection in a virtual list using `react-virtuoso` diff --git a/docs/guides/virtual-list.zh-CN.md b/docs/guides/virtual-list.zh-CN.md index dc1b647..1d7f28b 100644 --- a/docs/guides/virtual-list.zh-CN.md +++ b/docs/guides/virtual-list.zh-CN.md @@ -6,6 +6,6 @@ group: order: 7 --- -### 配合 `react-window` 在虚拟列表中进行框选 +### 配合 `react-virtuoso` 在虚拟列表中进行框选 diff --git a/package.json b/package.json index d462437..b07ef2d 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "prettier-plugin-packagejson": "^2", "react": "^18", "react-dom": "^18", - "react-window": "^1.8.9", + "react-virtuoso": "^4.5.0", "semantic-release": "^21", "stylelint": "^15", "typescript": "^5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e54e772..b684b76 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,9 +102,9 @@ devDependencies: react-dom: specifier: ^18 version: 18.2.0(react@18.2.0) - react-window: - specifier: ^1.8.9 - version: 1.8.9(react-dom@18.2.0)(react@18.2.0) + react-virtuoso: + specifier: ^4.5.0 + version: 4.5.0(react-dom@18.2.0)(react@18.2.0) semantic-release: specifier: ^21 version: 21.0.7 @@ -10955,10 +10955,6 @@ packages: fs-monkey: 1.0.4 dev: true - /memoize-one@5.2.1: - resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} - dev: true - /meow@10.1.5: resolution: {integrity: sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -13939,15 +13935,13 @@ packages: refractor: 3.6.0 dev: true - /react-window@1.8.9(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-+Eqx/fj1Aa5WnhRfj9dJg4VYATGwIUP2ItwItiJ6zboKWA6EX3lYDAXfGF2hyNqplEprhbtjbipiADEcwQ823Q==} - engines: {node: '>8.0.0'} + /react-virtuoso@4.5.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-OMP6XrzJMMos1vbJZC16RxGW7utAxUMP7i5PNPi6epBNVH7nz+CF/DlmecNBep5wyjLud51dQ5epjb2A0w9W/Q==} + engines: {node: '>=10'} peerDependencies: - react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react: '>=16 || >=17 || >= 18' + react-dom: '>=16 || >=17 || >= 18' dependencies: - '@babel/runtime': 7.22.10 - memoize-one: 5.2.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true diff --git a/src/Selectable.tsx b/src/Selectable.tsx index ec0d690..0347c43 100644 --- a/src/Selectable.tsx +++ b/src/Selectable.tsx @@ -10,6 +10,7 @@ import React, { useState, } from 'react'; import { SelectableContext } from './context'; +import useLatest from './hooks/useLatest'; interface SelectableProps { defaultValue?: React.Key[]; @@ -61,31 +62,11 @@ const Selectable = forwardRef( const moveClient = useRef({ x: 0, y: 0 }); const [value, setValue] = useMergedState(defaultValue || [], { value: propsValue, - onChange: (newValue) => { - if (onEnd) { - const added: React.Key[] = []; - const removed: React.Key[] = []; - - newValue.forEach((i) => { - if (value.includes(i)) { - if (mode === 'remove' || mode === 'reverse') { - removed.push(i); - } - } else { - if (mode === 'add' || mode === 'reverse') { - added.push(i); - } - } - }); - onEnd(newValue, { added, removed }); - } - }, }); - const startCoordsRef = useRef(startCoords); - startCoordsRef.current = startCoords; - const isDraggingRef = useRef(isDragging); - isDraggingRef.current = isDragging; + const startCoordsRef = useLatest(startCoords); + const isDraggingRef = useLatest(isDragging); + const selectFromInsideRef = useLatest(selectFromInside); const top = Math.max(0, Math.min(startCoords.y, moveCoords.y)); const left = Math.max(0, Math.min(startCoords.x, moveCoords.x)); @@ -111,15 +92,44 @@ const Selectable = forwardRef( })); const handleStart = useEvent(() => { - if (!isDragging) { + if (!isDraggingRef.current) { onStart?.(); } }); + const handleEnd = useEvent((newValue: React.Key[]) => { + if (onEnd) { + const added: React.Key[] = []; + const removed: React.Key[] = []; + + newValue.forEach((i) => { + if (value.includes(i)) { + if (mode === 'remove' || mode === 'reverse') { + removed.push(i); + } + } else { + if (mode === 'add' || mode === 'reverse') { + added.push(i); + } + } + }); + onEnd(newValue, { added, removed }); + } + }); + + const handleReset = () => { + setIsDragging(false); + setStartTarget(null); + isStart.current = false; + startInside.current = false; + selectingValue.current = []; + }; + useEffect(() => { const container = getContainer(); if (disabled || !container) { + handleReset(); return; } @@ -143,7 +153,7 @@ const Selectable = forwardRef( const height = Math.abs(startCoordsRef.current.y - y); // prevent trigger when click too fast // https://github.com/linxianxi/react-selectable-box/issues/5 - if (width > 1 || height > 1) { + if (!isDraggingRef.current && (width > 1 || height > 1)) { setIsDragging(true); handleStart(); } @@ -159,18 +169,15 @@ const Selectable = forwardRef( if (isDraggingRef.current) { setValue(selectingValue.current); - selectingValue.current = []; - setIsDragging(false); + handleEnd(selectingValue.current); } - setStartTarget(null); - isStart.current = false; - startInside.current = false; + handleReset(); }; const scrollListenerElement = container === document.body ? document : container; const onMouseDown = (e: MouseEvent) => { - if (!selectFromInside) { + if (!selectFromInsideRef.current) { setStartTarget(e.target as HTMLElement); } @@ -195,7 +202,7 @@ const Selectable = forwardRef( window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); }; - }, [disabled, selectFromInside]); + }, [disabled]); const container = getContainer(); diff --git a/src/hooks/useLatest.ts b/src/hooks/useLatest.ts new file mode 100644 index 0000000..0cfbd2a --- /dev/null +++ b/src/hooks/useLatest.ts @@ -0,0 +1,8 @@ +import { useRef } from 'react'; + +export default function useLatest(value: T) { + const ref = useRef(value); + ref.current = value; + + return ref; +}