From 625f8d13e81039ab302fdb509968bb5ff16123de Mon Sep 17 00:00:00 2001 From: Alissa Date: Wed, 29 Mar 2023 17:54:56 +0300 Subject: [PATCH 01/32] active and active event ok --- package.json | 1 + packages/core/package.json | 5 +- .../src/components/DndContext/DndContext.tsx | 41 ++--- .../src/components/DndContext/activeAPI.ts | 80 +++++++++ .../core/src/components/my/stateMachine.ts | 88 ++++++++++ packages/core/src/hooks/useDraggable.ts | 9 +- packages/core/src/hooks/useDroppable.ts | 10 +- packages/core/src/store/reducer.ts | 10 +- packages/core/src/store/types.ts | 7 +- .../1 - Core/Draggable/1-Draggable.story.tsx | 13 +- .../Draggable/3-MultipleDraggable.story.tsx | 162 ++++++++++++++++++ yarn.lock | 10 ++ 12 files changed, 384 insertions(+), 52 deletions(-) create mode 100644 packages/core/src/components/DndContext/activeAPI.ts create mode 100644 packages/core/src/components/my/stateMachine.ts create mode 100644 stories/1 - Core/Draggable/3-MultipleDraggable.story.tsx diff --git a/package.json b/package.json index 3569d4dd..f19de95d 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@types/classnames": "^2.2.11", "@types/react": "^16.9.43", "@types/react-dom": "^16.9.8", + "@types/use-sync-external-store": "^0.0.3", "babel-jest": "^27.0.2", "babel-loader": "^8.2.1", "chromatic": "^5.4.0", diff --git a/packages/core/package.json b/packages/core/package.json index 642fdde9..0322f906 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -30,9 +30,10 @@ "react-dom": ">=16.8.0" }, "dependencies": { - "tslib": "^2.0.0", "@dnd-kit/accessibility": "^3.0.0", - "@dnd-kit/utilities": "^3.2.1" + "@dnd-kit/utilities": "^3.2.1", + "tslib": "^2.0.0", + "use-sync-external-store": "^1.2.0" }, "publishConfig": { "access": "public" diff --git a/packages/core/src/components/DndContext/DndContext.tsx b/packages/core/src/components/DndContext/DndContext.tsx index 9d5df10d..3d4c0ba6 100644 --- a/packages/core/src/components/DndContext/DndContext.tsx +++ b/packages/core/src/components/DndContext/DndContext.tsx @@ -78,12 +78,13 @@ import { ScreenReaderInstructions, } from '../Accessibility'; -import {defaultData, defaultSensors} from './defaults'; +import {defaultSensors} from './defaults'; import { useLayoutShiftScrollCompensation, useMeasuringConfiguration, } from './hooks'; import type {MeasuringConfiguration} from './types'; +import {createActiveAPI} from './activeAPI'; export interface Props { id?: string; @@ -149,29 +150,23 @@ export const DndContext = memo(function DndContext({ const [status, setStatus] = useState(Status.Uninitialized); const isInitialized = status === Status.Initialized; const { - draggable: {active: activeId, nodes: draggableNodes, translate}, + draggable: {translate}, droppable: {containers: droppableContainers}, } = state; - const node = activeId ? draggableNodes.get(activeId) : null; const activeRects = useRef({ initial: null, translated: null, }); - const active = useMemo( - () => - activeId != null - ? { - id: activeId, - // It's possible for the active node to unmount while dragging - data: node?.data ?? defaultData, - rect: activeRects, - } - : null, - [activeId, node] - ); + + const activeAPI = useMemo(() => createActiveAPI(activeRects), []); + const draggableNodes = activeAPI.draggableNodes; + const active = activeAPI.useActive(); + const activeId = active?.id || null; + + const activatorEvent = activeAPI.useActivatorEvent(); + const activeRef = useRef(null); const [activeSensor, setActiveSensor] = useState(null); - const [activatorEvent, setActivatorEvent] = useState(null); const latestProps = useLatestValue(props, Object.values(props)); const draggableDescribedById = useUniqueId(`DndDescribedBy`, id); const enabledDroppableContainers = useMemo( @@ -365,6 +360,7 @@ export const DndContext = memo(function DndContext({ unstable_batchedUpdates(() => { onDragStart?.(event); setStatus(Status.Initializing); + activeAPI.setActive(id); dispatch({ type: Action.DragStart, initialCoordinates, @@ -385,7 +381,7 @@ export const DndContext = memo(function DndContext({ unstable_batchedUpdates(() => { setActiveSensor(sensorInstance); - setActivatorEvent(event.nativeEvent); + activeAPI.setActivatorEvent(event.nativeEvent); }); function createHandler(type: Action.DragEnd | Action.DragCancel) { @@ -417,11 +413,12 @@ export const DndContext = memo(function DndContext({ activeRef.current = null; unstable_batchedUpdates(() => { + activeAPI.setActive(null); dispatch({type}); setStatus(Status.Uninitialized); setOver(null); setActiveSensor(null); - setActivatorEvent(null); + activeAPI.setActivatorEvent(null); const eventName = type === Action.DragEnd ? 'onDragEnd' : 'onDragCancel'; @@ -665,9 +662,10 @@ export const DndContext = memo(function DndContext({ const internalContext = useMemo(() => { const context: InternalContextDescriptor = { - activatorEvent, + useMyActive: activeAPI.useMyActive, + useHasActive: activeAPI.useHasActive, + useMyActivatorEvent: activeAPI.useMyActivatorEvent, activators, - active, activeNodeRect, ariaDescribedById: { draggable: draggableDescribedById, @@ -680,15 +678,14 @@ export const DndContext = memo(function DndContext({ return context; }, [ - activatorEvent, activators, - active, activeNodeRect, dispatch, draggableDescribedById, draggableNodes, over, measureDroppableContainers, + activeAPI, ]); return ( diff --git a/packages/core/src/components/DndContext/activeAPI.ts b/packages/core/src/components/DndContext/activeAPI.ts new file mode 100644 index 00000000..11d6716d --- /dev/null +++ b/packages/core/src/components/DndContext/activeAPI.ts @@ -0,0 +1,80 @@ +import type {MutableRefObject} from 'react'; +import {useSyncExternalStore} from 'use-sync-external-store/shim'; +import type {Active} from '../../store'; + +import type {UniqueIdentifier, ClientRect} from '../../types'; +import {defaultData} from './defaults'; + +type Rects = MutableRefObject<{ + initial: ClientRect | null; + translated: ClientRect | null; +}>; + +export function createActiveAPI(rect: Rects) { + let activeId: UniqueIdentifier | null = null; + let active: Active | null = null; + + let activatorEvent: Event | null = null; + const draggableNodes = new Map(); + const activeRects = rect; + + const registry: (() => void)[] = []; + + function subscribe(listener: () => void) { + registry.push(listener); + return () => { + registry.splice(registry.indexOf(listener), 1); + }; + } + + function getActiveInfo() { + if (!activeId) return null; + const node = draggableNodes.get(activeId); + return { + id: activeId, + rect: activeRects, + data: node ? node.data : defaultData, + }; + } + + return { + draggableNodes, + setActive: function (id: UniqueIdentifier | null) { + if (activeId === id) return; + activeId = id; + active = getActiveInfo(); + registry.forEach((li) => li()); + }, + + setActivatorEvent: function (event: Event | null) { + activatorEvent = event; + registry.forEach((li) => li()); + }, + + useIsDragging: function (id: UniqueIdentifier) { + return useSyncExternalStore(subscribe, () => activeId === id); + }, + + useHasActive: function () { + return useSyncExternalStore(subscribe, () => activeId !== null); + }, + useActive: function () { + return useSyncExternalStore(subscribe, () => active); + }, + + useMyActive: function (id: UniqueIdentifier) { + return useSyncExternalStore(subscribe, () => + activeId === id ? active : null + ); + }, + + useActivatorEvent: function () { + return useSyncExternalStore(subscribe, () => activatorEvent); + }, + useMyActivatorEvent: function (id: UniqueIdentifier) { + return useSyncExternalStore(subscribe, () => + activeId === id ? activatorEvent : null + ); + }, + }; +} diff --git a/packages/core/src/components/my/stateMachine.ts b/packages/core/src/components/my/stateMachine.ts new file mode 100644 index 00000000..ce923075 --- /dev/null +++ b/packages/core/src/components/my/stateMachine.ts @@ -0,0 +1,88 @@ +import type {MutableRefObject} from 'react'; +import type {DraggableNodes, Over} from '../../store'; +import type { + Coordinates, + DragStartEvent, + UniqueIdentifier, + ClientRect, + Translate, + DragEndEvent, +} from '../../types'; +import type {Collision} from '../../utilities'; +import {defaultData} from '../DndContext/defaults'; + +enum Status { + Uninitialized, + Initializing, + Initialized, +} + +export class Api { + state = {}; + draggableNodes: DraggableNodes = new Map(); + status: Status = Status.Uninitialized; + active: UniqueIdentifier | null = null; + initialCoordinates: Coordinates = {x: 0, y: 0}; + + startDrag( + id: UniqueIdentifier | null, + activeRects: MutableRefObject<{ + initial: ClientRect | null; + translated: ClientRect | null; + }>, + initialCoordinates: Coordinates, + onDragStart?: (event: DragStartEvent) => void + ) { + if (id == null) { + return; + } + const draggableNode = this.draggableNodes.get(id); + + if (!draggableNode) { + return; + } + const event: DragStartEvent = { + active: {id, data: draggableNode.data, rect: activeRects}, + }; + + onDragStart?.(event); + this.status = Status.Initializing; + this.active = id; + this.initialCoordinates = initialCoordinates; + } + + endDrag( + activeRects: MutableRefObject<{ + initial: ClientRect | null; + translated: ClientRect | null; + }>, + type: 'onDragEnd' | 'onDragCancel', + sensorState: { + over: Over | null; + collisions: Collision[] | null; + activatorEvent: Event | null; + delta: Translate | null; + }, + onDragEnd?: (event: DragEndEvent) => void + ) { + const event = this.active + ? { + ...sensorState, + active: { + id: this.active, + data: this.draggableNodes.get(this.active)?.data ?? defaultData, + rect: activeRects, + }, + } + : null; + const shouldFireEvent = this.active && sensorState.delta; + + this.active = null; + this.initialCoordinates = {x: 0, y: 0}; + this.status = Status.Uninitialized; + + if (shouldFireEvent) { + onDragEnd?.(event as DragEndEvent); + } + } +} diff --git a/packages/core/src/hooks/useDraggable.ts b/packages/core/src/hooks/useDraggable.ts index 48fb19b0..08a6e45c 100644 --- a/packages/core/src/hooks/useDraggable.ts +++ b/packages/core/src/hooks/useDraggable.ts @@ -49,8 +49,8 @@ export function useDraggable({ const key = useUniqueId(ID_PREFIX); const { activators, - activatorEvent, - active, + useMyActivatorEvent, + useMyActive, activeNodeRect, ariaDescribedById, draggableNodes, @@ -61,7 +61,9 @@ export function useDraggable({ roleDescription = 'draggable', tabIndex = 0, } = attributes ?? {}; - const isDragging = active?.id === id; + const active = useMyActive(id); + const isDragging = active !== null; + const activatorEvent = useMyActivatorEvent(id); const transform: Transform | null = useContext( isDragging ? ActiveDraggableContext : NullContext ); @@ -106,6 +108,7 @@ export function useDraggable({ ); return { + //active and activatorEvent will by null if this isn't the active node active, activatorEvent, activeNodeRect, diff --git a/packages/core/src/hooks/useDroppable.ts b/packages/core/src/hooks/useDroppable.ts index 7939f15d..065f8bea 100644 --- a/packages/core/src/hooks/useDroppable.ts +++ b/packages/core/src/hooks/useDroppable.ts @@ -43,9 +43,9 @@ export function useDroppable({ resizeObserverConfig, }: UseDroppableArguments) { const key = useUniqueId(ID_PREFIX); - const {active, dispatch, over, measureDroppableContainers} = useContext( - InternalContext - ); + const {useHasActive, dispatch, over, measureDroppableContainers} = + useContext(InternalContext); + const hasActive = useHasActive(); const previous = useRef({disabled}); const resizeObserverConnected = useRef(false); const rect = useRef(null); @@ -84,7 +84,7 @@ export function useDroppable({ ); const resizeObserver = useResizeObserver({ callback: handleResize, - disabled: resizeObserverDisabled || !active, + disabled: resizeObserverDisabled || !hasActive, }); const handleNodeChange = useCallback( (newElement: HTMLElement | null, previousElement: HTMLElement | null) => { @@ -155,7 +155,7 @@ export function useDroppable({ }, [id, key, disabled, dispatch]); return { - active, + //I removed the active from here, it forces all droppable to re-render when active changes. rect, isOver: over?.id === id, node: nodeRef, diff --git a/packages/core/src/store/reducer.ts b/packages/core/src/store/reducer.ts index fa34ba9e..f6ba5975 100644 --- a/packages/core/src/store/reducer.ts +++ b/packages/core/src/store/reducer.ts @@ -5,9 +5,7 @@ import type {State} from './types'; export function getInitialState(): State { return { draggable: { - active: null, initialCoordinates: {x: 0, y: 0}, - nodes: new Map(), translate: {x: 0, y: 0}, }, droppable: { @@ -24,13 +22,12 @@ export function reducer(state: State, action: Actions): State { draggable: { ...state.draggable, initialCoordinates: action.initialCoordinates, - active: action.active, }, }; case Action.DragMove: - if (!state.draggable.active) { - return state; - } + // if (!state.draggable.active) { + // return state; + // } return { ...state, @@ -48,7 +45,6 @@ export function reducer(state: State, action: Actions): State { ...state, draggable: { ...state.draggable, - active: null, initialCoordinates: {x: 0, y: 0}, translate: {x: 0, y: 0}, }, diff --git a/packages/core/src/store/types.ts b/packages/core/src/store/types.ts index b9b21f91..6674f264 100644 --- a/packages/core/src/store/types.ts +++ b/packages/core/src/store/types.ts @@ -67,9 +67,7 @@ export interface State { containers: DroppableContainers; }; draggable: { - active: UniqueIdentifier | null; initialCoordinates: Coordinates; - nodes: DraggableNodes; translate: Coordinates; }; } @@ -99,9 +97,10 @@ export interface PublicContextDescriptor { } export interface InternalContextDescriptor { - activatorEvent: Event | null; + useMyActivatorEvent: (id: UniqueIdentifier) => Event | null; activators: SyntheticListeners; - active: Active | null; + useMyActive: (id: UniqueIdentifier) => Active | null; + useHasActive: () => boolean; activeNodeRect: ClientRect | null; ariaDescribedById: { draggable: string; diff --git a/stories/1 - Core/Draggable/1-Draggable.story.tsx b/stories/1 - Core/Draggable/1-Draggable.story.tsx index c62dc8ca..500164cb 100644 --- a/stories/1 - Core/Draggable/1-Draggable.story.tsx +++ b/stories/1 - Core/Draggable/1-Draggable.story.tsx @@ -112,15 +112,10 @@ function DraggableItem({ handle, buttonStyle, }: DraggableItemProps) { - const { - attributes, - isDragging, - listeners, - setNodeRef, - transform, - } = useDraggable({ - id: 'draggable', - }); + const {attributes, isDragging, listeners, setNodeRef, transform} = + useDraggable({ + id: 'draggable', + }); return ( ({ + '1': defaultCoordinates, + '2': defaultCoordinates, + '3': defaultCoordinates, + }); + // const [{x, y}, setCoordinates] = useState(defaultCoordinates); + const mouseSensor = useSensor(MouseSensor, { + activationConstraint, + }); + const touchSensor = useSensor(TouchSensor, { + activationConstraint, + }); + const keyboardSensor = useSensor(KeyboardSensor, {}); + const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor); + + return ( + { + setCoordinates((state) => { + return { + ...state, + [active.id]: { + x: state[active.id].x + delta.x, + y: state[active.id].y + delta.y, + }, + }; + }); + }} + modifiers={modifiers} + > + + + + + + + ); +} + +interface DraggableItemProps { + label: string; + handle?: boolean; + style?: React.CSSProperties; + buttonStyle?: React.CSSProperties; + axis?: Axis; + top?: number; + left?: number; + id: string; +} + +function DraggableItem({ + id, + axis, + label, + style, + top, + left, + handle, + buttonStyle, +}: DraggableItemProps) { + const {attributes, isDragging, listeners, setNodeRef, transform} = + useDraggable({ + id: id, + }); + + console.log('render draggable', id); + + return ( + + ); +} + +const MemoDraggableItem = React.memo(DraggableItem); + +export const BasicSetup = () => ; diff --git a/yarn.lock b/yarn.lock index 2dc43fd1..027ecd66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4318,6 +4318,11 @@ resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/use-sync-external-store@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" + integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== + "@types/webpack-env@^1.16.0": version "1.16.0" resolved "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.16.0.tgz" @@ -16561,6 +16566,11 @@ use-sidecar@^1.0.1: detect-node-es "^1.0.0" tslib "^1.9.3" +use-sync-external-store@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + use@^3.1.0: version "3.1.1" resolved "https://registry.npmjs.org/use/-/use-3.1.1.tgz" From dd88743ed771794f0c2e880a385fd09386f81b2d Mon Sep 17 00:00:00 2001 From: Alissa Date: Wed, 29 Mar 2023 18:52:26 +0300 Subject: [PATCH 02/32] no re-render non dragged items on drag start / end --- .../Accessibility/components/RestoreFocus.tsx | 6 ++- .../src/components/DndContext/DndContext.tsx | 50 ++++++++++++------- .../components/DndContext/useActiveRects.tsx | 37 ++++++++++++++ packages/core/src/hooks/useDraggable.ts | 3 +- packages/core/src/store/context.ts | 33 +++++++++--- packages/core/src/store/types.ts | 6 ++- .../Draggable/3-MultipleDraggable.story.tsx | 18 ++++--- 7 files changed, 115 insertions(+), 38 deletions(-) create mode 100644 packages/core/src/components/DndContext/useActiveRects.tsx diff --git a/packages/core/src/components/Accessibility/components/RestoreFocus.tsx b/packages/core/src/components/Accessibility/components/RestoreFocus.tsx index 928f248c..bbac7de2 100644 --- a/packages/core/src/components/Accessibility/components/RestoreFocus.tsx +++ b/packages/core/src/components/Accessibility/components/RestoreFocus.tsx @@ -12,7 +12,11 @@ interface Props { } export function RestoreFocus({disabled}: Props) { - const {active, activatorEvent, draggableNodes} = useContext(InternalContext); + const {useGloablActive, useGlobalActivatorEvent, draggableNodes} = + useContext(InternalContext); + const active = useGloablActive(); + const activatorEvent = useGlobalActivatorEvent(); + const previousActivatorEvent = usePrevious(activatorEvent); const previousActiveId = usePrevious(active?.id); diff --git a/packages/core/src/components/DndContext/DndContext.tsx b/packages/core/src/components/DndContext/DndContext.tsx index 3d4c0ba6..2fdf43d8 100644 --- a/packages/core/src/components/DndContext/DndContext.tsx +++ b/packages/core/src/components/DndContext/DndContext.tsx @@ -31,11 +31,9 @@ import { import {DndMonitorContext, useDndMonitorProvider} from '../DndMonitor'; import { useAutoScroller, - useCachedNode, useCombineActivators, useDragOverlayMeasuring, useDroppableMeasuring, - useInitialRect, useRect, useRectDelta, useRects, @@ -85,6 +83,7 @@ import { } from './hooks'; import type {MeasuringConfiguration} from './types'; import {createActiveAPI} from './activeAPI'; +import {useActiveRects} from './useActiveRects'; export interface Props { id?: string; @@ -180,16 +179,24 @@ export const DndContext = memo(function DndContext({ dependencies: [translate.x, translate.y], config: measuringConfiguration.droppable, }); - const activeNode = useCachedNode(draggableNodes, activeId); + + const activeNodeStuff = useActiveRects( + draggableNodes, + measuringConfiguration, + active?.id || null + ); + const activeNode = activeNodeStuff?.activeNode || null; + const initialActiveNodeRect = activeNodeStuff?.initialActiveNodeRect || null; + const activeNodeRect = activeNodeStuff?.activeNodeRect || null; + const containerNodeRect = useRect( + activeNode ? activeNode.parentElement : null + ); + const activationCoordinates = useMemo( () => (activatorEvent ? getEventCoordinates(activatorEvent) : null), [activatorEvent] ); const autoScrollOptions = getAutoScrollerOptions(); - const initialActiveNodeRect = useInitialRect( - activeNode, - measuringConfiguration.draggable.measure - ); useLayoutShiftScrollCompensation({ activeNode: activeId ? draggableNodes.get(activeId) : null, @@ -198,14 +205,6 @@ export const DndContext = memo(function DndContext({ measure: measuringConfiguration.draggable.measure, }); - const activeNodeRect = useRect( - activeNode, - measuringConfiguration.draggable.measure, - initialActiveNodeRect - ); - const containerNodeRect = useRect( - activeNode ? activeNode.parentElement : null - ); const sensorContext = useRef({ activatorEvent: null, active: null, @@ -664,9 +663,18 @@ export const DndContext = memo(function DndContext({ const context: InternalContextDescriptor = { useMyActive: activeAPI.useMyActive, useHasActive: activeAPI.useHasActive, + useGloablActive: activeAPI.useActive, useMyActivatorEvent: activeAPI.useMyActivatorEvent, + useGlobalActivatorEvent: activeAPI.useActivatorEvent, + useMyActiveNodeRect: (id: UniqueIdentifier) => { + const stuff = useActiveRects( + draggableNodes, + measuringConfiguration, + id + ); + return stuff?.activeNodeRect || null; + }, activators, - activeNodeRect, ariaDescribedById: { draggable: draggableDescribedById, }, @@ -678,14 +686,18 @@ export const DndContext = memo(function DndContext({ return context; }, [ + activeAPI.useMyActive, + activeAPI.useHasActive, + activeAPI.useActive, + activeAPI.useMyActivatorEvent, + activeAPI.useActivatorEvent, activators, - activeNodeRect, - dispatch, draggableDescribedById, + dispatch, draggableNodes, over, measureDroppableContainers, - activeAPI, + measuringConfiguration, ]); return ( diff --git a/packages/core/src/components/DndContext/useActiveRects.tsx b/packages/core/src/components/DndContext/useActiveRects.tsx new file mode 100644 index 00000000..73fc4bfc --- /dev/null +++ b/packages/core/src/components/DndContext/useActiveRects.tsx @@ -0,0 +1,37 @@ +import type {DeepRequired} from '@dnd-kit/utilities'; +import {useMemo} from 'react'; +import {useCachedNode, useInitialRect, useRect} from '../../hooks/utilities'; +import type {DraggableNodes} from '../../store'; +import type {UniqueIdentifier} from '../../types'; +import type {MeasuringConfiguration} from './types'; + +export function useActiveRects( + draggableNodes: DraggableNodes, + measuringConfiguration: DeepRequired, + activeId: UniqueIdentifier | null +) { + const activeNode = useCachedNode(draggableNodes, activeId); + + const initialActiveNodeRect = useInitialRect( + activeNode, + measuringConfiguration.draggable.measure + ); + + const activeNodeRect = useRect( + activeNode, + measuringConfiguration.draggable.measure, + initialActiveNodeRect + ); + + const value = useMemo(() => { + return activeNode + ? { + activeNode, + activeNodeRect, + initialActiveNodeRect, + } + : null; + }, [activeNode, activeNodeRect, initialActiveNodeRect]); + + return value; +} diff --git a/packages/core/src/hooks/useDraggable.ts b/packages/core/src/hooks/useDraggable.ts index 08a6e45c..ab887ebe 100644 --- a/packages/core/src/hooks/useDraggable.ts +++ b/packages/core/src/hooks/useDraggable.ts @@ -51,7 +51,7 @@ export function useDraggable({ activators, useMyActivatorEvent, useMyActive, - activeNodeRect, + useMyActiveNodeRect, ariaDescribedById, draggableNodes, over, @@ -64,6 +64,7 @@ export function useDraggable({ const active = useMyActive(id); const isDragging = active !== null; const activatorEvent = useMyActivatorEvent(id); + const activeNodeRect = useMyActiveNodeRect(id); const transform: Transform | null = useContext( isDragging ? ActiveDraggableContext : NullContext ); diff --git a/packages/core/src/store/context.ts b/packages/core/src/store/context.ts index bb624ec8..50b1bb48 100644 --- a/packages/core/src/store/context.ts +++ b/packages/core/src/store/context.ts @@ -3,7 +3,12 @@ import {createContext} from 'react'; import {noop} from '../utilities/other'; import {defaultMeasuringConfiguration} from '../components/DndContext/defaults'; import {DroppableContainersMap} from './constructors'; -import type {InternalContextDescriptor, PublicContextDescriptor} from './types'; +import type { + Active, + InternalContextDescriptor, + PublicContextDescriptor, +} from './types'; +import type {ClientRect} from '../types'; export const defaultPublicContext: PublicContextDescriptor = { activatorEvent: null, @@ -32,10 +37,7 @@ export const defaultPublicContext: PublicContextDescriptor = { }; export const defaultInternalContext: InternalContextDescriptor = { - activatorEvent: null, activators: [], - active: null, - activeNodeRect: null, ariaDescribedById: { draggable: '', }, @@ -43,12 +45,29 @@ export const defaultInternalContext: InternalContextDescriptor = { draggableNodes: new Map(), over: null, measureDroppableContainers: noop, + useMyActive: function (): Active | null { + throw new Error('Function not implemented.'); + }, + useGloablActive: function (): Active | null { + throw new Error('Function not implemented.'); + }, + useHasActive: function (): boolean { + throw new Error('Function not implemented.'); + }, + useMyActivatorEvent: function (): Event | null { + throw new Error('Function not implemented.'); + }, + useGlobalActivatorEvent: function (): Event | null { + throw new Error('Function not implemented.'); + }, + useMyActiveNodeRect: function (): ClientRect | null { + throw new Error('Function not implemented.'); + }, }; export const InternalContext = createContext( defaultInternalContext ); -export const PublicContext = createContext( - defaultPublicContext -); +export const PublicContext = + createContext(defaultPublicContext); diff --git a/packages/core/src/store/types.ts b/packages/core/src/store/types.ts index 6674f264..ea2acdff 100644 --- a/packages/core/src/store/types.ts +++ b/packages/core/src/store/types.ts @@ -97,11 +97,13 @@ export interface PublicContextDescriptor { } export interface InternalContextDescriptor { - useMyActivatorEvent: (id: UniqueIdentifier) => Event | null; activators: SyntheticListeners; useMyActive: (id: UniqueIdentifier) => Active | null; + useGloablActive: () => Active | null; useHasActive: () => boolean; - activeNodeRect: ClientRect | null; + useMyActivatorEvent: (id: UniqueIdentifier) => Event | null; + useGlobalActivatorEvent: () => Event | null; + useMyActiveNodeRect: (id: UniqueIdentifier) => ClientRect | null; ariaDescribedById: { draggable: string; }; diff --git a/stories/1 - Core/Draggable/3-MultipleDraggable.story.tsx b/stories/1 - Core/Draggable/3-MultipleDraggable.story.tsx index 1a342d92..6249b7e9 100644 --- a/stories/1 - Core/Draggable/3-MultipleDraggable.story.tsx +++ b/stories/1 - Core/Draggable/3-MultipleDraggable.story.tsx @@ -1,4 +1,4 @@ -import React, {useState} from 'react'; +import React, {useMemo, useState} from 'react'; import { DndContext, useDraggable, @@ -49,13 +49,15 @@ function DraggableStory({ '3': defaultCoordinates, }); // const [{x, y}, setCoordinates] = useState(defaultCoordinates); - const mouseSensor = useSensor(MouseSensor, { - activationConstraint, - }); - const touchSensor = useSensor(TouchSensor, { - activationConstraint, - }); - const keyboardSensor = useSensor(KeyboardSensor, {}); + const sensorOptions = useMemo( + () => ({ + activationConstraint, + }), + [activationConstraint] + ); + const mouseSensor = useSensor(MouseSensor, sensorOptions); + const touchSensor = useSensor(TouchSensor, sensorOptions); + const keyboardSensor = useSensor(KeyboardSensor); const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor); return ( From 92419894f97b2941b7ed0e289a234a710ddaeff5 Mon Sep 17 00:00:00 2001 From: Alissa Date: Wed, 29 Mar 2023 19:24:39 +0300 Subject: [PATCH 03/32] rename --- .../core/src/components/DndContext/DndContext.tsx | 15 ++++++++------- ...ActiveRects.tsx => useActiveNodeDomValues.tsx} | 2 +- .../Draggable/3-MultipleDraggable.story.tsx | 4 +++- 3 files changed, 12 insertions(+), 9 deletions(-) rename packages/core/src/components/DndContext/{useActiveRects.tsx => useActiveNodeDomValues.tsx} (96%) diff --git a/packages/core/src/components/DndContext/DndContext.tsx b/packages/core/src/components/DndContext/DndContext.tsx index 2fdf43d8..df815589 100644 --- a/packages/core/src/components/DndContext/DndContext.tsx +++ b/packages/core/src/components/DndContext/DndContext.tsx @@ -83,7 +83,7 @@ import { } from './hooks'; import type {MeasuringConfiguration} from './types'; import {createActiveAPI} from './activeAPI'; -import {useActiveRects} from './useActiveRects'; +import {useActiveNodeDomValues} from './useActiveNodeDomValues'; export interface Props { id?: string; @@ -180,14 +180,15 @@ export const DndContext = memo(function DndContext({ config: measuringConfiguration.droppable, }); - const activeNodeStuff = useActiveRects( + const activeNodeDomValues = useActiveNodeDomValues( draggableNodes, measuringConfiguration, active?.id || null ); - const activeNode = activeNodeStuff?.activeNode || null; - const initialActiveNodeRect = activeNodeStuff?.initialActiveNodeRect || null; - const activeNodeRect = activeNodeStuff?.activeNodeRect || null; + const activeNode = activeNodeDomValues?.activeNode || null; + const initialActiveNodeRect = + activeNodeDomValues?.initialActiveNodeRect || null; + const activeNodeRect = activeNodeDomValues?.activeNodeRect || null; const containerNodeRect = useRect( activeNode ? activeNode.parentElement : null ); @@ -667,12 +668,12 @@ export const DndContext = memo(function DndContext({ useMyActivatorEvent: activeAPI.useMyActivatorEvent, useGlobalActivatorEvent: activeAPI.useActivatorEvent, useMyActiveNodeRect: (id: UniqueIdentifier) => { - const stuff = useActiveRects( + const domValues = useActiveNodeDomValues( draggableNodes, measuringConfiguration, id ); - return stuff?.activeNodeRect || null; + return domValues?.activeNodeRect || null; }, activators, ariaDescribedById: { diff --git a/packages/core/src/components/DndContext/useActiveRects.tsx b/packages/core/src/components/DndContext/useActiveNodeDomValues.tsx similarity index 96% rename from packages/core/src/components/DndContext/useActiveRects.tsx rename to packages/core/src/components/DndContext/useActiveNodeDomValues.tsx index 73fc4bfc..a8d44362 100644 --- a/packages/core/src/components/DndContext/useActiveRects.tsx +++ b/packages/core/src/components/DndContext/useActiveNodeDomValues.tsx @@ -5,7 +5,7 @@ import type {DraggableNodes} from '../../store'; import type {UniqueIdentifier} from '../../types'; import type {MeasuringConfiguration} from './types'; -export function useActiveRects( +export function useActiveNodeDomValues( draggableNodes: DraggableNodes, measuringConfiguration: DeepRequired, activeId: UniqueIdentifier | null diff --git a/stories/1 - Core/Draggable/3-MultipleDraggable.story.tsx b/stories/1 - Core/Draggable/3-MultipleDraggable.story.tsx index 6249b7e9..7122a49b 100644 --- a/stories/1 - Core/Draggable/3-MultipleDraggable.story.tsx +++ b/stories/1 - Core/Draggable/3-MultipleDraggable.story.tsx @@ -48,7 +48,6 @@ function DraggableStory({ '2': defaultCoordinates, '3': defaultCoordinates, }); - // const [{x, y}, setCoordinates] = useState(defaultCoordinates); const sensorOptions = useMemo( () => ({ activationConstraint, @@ -159,6 +158,9 @@ function DraggableItem({ ); } +//we are memoizing the draggable item to prevent it all items from re-rendering when one of them changes coordinates. +//so it will be easier to test +//changes in the context are ignored by the memoization const MemoDraggableItem = React.memo(DraggableItem); export const BasicSetup = () => ; From 38e3c18c838f3faa92e4a648dd554c441b804212 Mon Sep 17 00:00:00 2001 From: Alissa Date: Wed, 29 Mar 2023 19:34:23 +0300 Subject: [PATCH 04/32] remove active from reducer --- .../src/components/DndContext/DndContext.tsx | 18 ++++++++---------- packages/core/src/store/actions.ts | 11 ++++------- packages/core/src/store/reducer.ts | 13 ++++++------- packages/core/src/store/types.ts | 2 +- 4 files changed, 19 insertions(+), 25 deletions(-) diff --git a/packages/core/src/components/DndContext/DndContext.tsx b/packages/core/src/components/DndContext/DndContext.tsx index df815589..6856cfd2 100644 --- a/packages/core/src/components/DndContext/DndContext.tsx +++ b/packages/core/src/components/DndContext/DndContext.tsx @@ -362,9 +362,8 @@ export const DndContext = memo(function DndContext({ setStatus(Status.Initializing); activeAPI.setActive(id); dispatch({ - type: Action.DragStart, + type: Action.SetInitiailCoordinates, initialCoordinates, - active: id, }); dispatchMonitorEvent({type: 'onDragStart', event}); }); @@ -375,8 +374,8 @@ export const DndContext = memo(function DndContext({ coordinates, }); }, - onEnd: createHandler(Action.DragEnd), - onCancel: createHandler(Action.DragCancel), + onEnd: createHandler('DragEnd'), + onCancel: createHandler('DragCancel'), }); unstable_batchedUpdates(() => { @@ -384,7 +383,7 @@ export const DndContext = memo(function DndContext({ activeAPI.setActivatorEvent(event.nativeEvent); }); - function createHandler(type: Action.DragEnd | Action.DragCancel) { + function createHandler(type: 'DragEnd' | 'DragCancel') { return async function handler() { const {active, collisions, over, scrollAdjustedTranslate} = sensorContext.current; @@ -401,11 +400,11 @@ export const DndContext = memo(function DndContext({ over, }; - if (type === Action.DragEnd && typeof cancelDrop === 'function') { + if (type === 'DragEnd' && typeof cancelDrop === 'function') { const shouldCancel = await Promise.resolve(cancelDrop(event)); if (shouldCancel) { - type = Action.DragCancel; + type = 'DragCancel'; } } } @@ -414,14 +413,13 @@ export const DndContext = memo(function DndContext({ unstable_batchedUpdates(() => { activeAPI.setActive(null); - dispatch({type}); + dispatch({type: Action.ClearCoordinates}); setStatus(Status.Uninitialized); setOver(null); setActiveSensor(null); activeAPI.setActivatorEvent(null); - const eventName = - type === Action.DragEnd ? 'onDragEnd' : 'onDragCancel'; + const eventName = type === 'DragEnd' ? 'onDragEnd' : 'onDragCancel'; if (event) { const handler = latestProps.current[eventName]; diff --git a/packages/core/src/store/actions.ts b/packages/core/src/store/actions.ts index 3797f7fb..94fb0716 100644 --- a/packages/core/src/store/actions.ts +++ b/packages/core/src/store/actions.ts @@ -2,10 +2,9 @@ import type {Coordinates, UniqueIdentifier} from '../types'; import type {DroppableContainer} from './types'; export enum Action { - DragStart = 'dragStart', + SetInitiailCoordinates = 'setInitialCoordinates', DragMove = 'dragMove', - DragEnd = 'dragEnd', - DragCancel = 'dragCancel', + ClearCoordinates = 'clearCoordinates', DragOver = 'dragOver', RegisterDroppable = 'registerDroppable', SetDroppableDisabled = 'setDroppableDisabled', @@ -14,13 +13,11 @@ export enum Action { export type Actions = | { - type: Action.DragStart; - active: UniqueIdentifier; + type: Action.SetInitiailCoordinates; initialCoordinates: Coordinates; } | {type: Action.DragMove; coordinates: Coordinates} - | {type: Action.DragEnd} - | {type: Action.DragCancel} + | {type: Action.ClearCoordinates} | { type: Action.RegisterDroppable; element: DroppableContainer; diff --git a/packages/core/src/store/reducer.ts b/packages/core/src/store/reducer.ts index f6ba5975..22b6a25f 100644 --- a/packages/core/src/store/reducer.ts +++ b/packages/core/src/store/reducer.ts @@ -5,7 +5,7 @@ import type {State} from './types'; export function getInitialState(): State { return { draggable: { - initialCoordinates: {x: 0, y: 0}, + initialCoordinates: null, translate: {x: 0, y: 0}, }, droppable: { @@ -16,7 +16,7 @@ export function getInitialState(): State { export function reducer(state: State, action: Actions): State { switch (action.type) { - case Action.DragStart: + case Action.SetInitiailCoordinates: return { ...state, draggable: { @@ -25,9 +25,9 @@ export function reducer(state: State, action: Actions): State { }, }; case Action.DragMove: - // if (!state.draggable.active) { - // return state; - // } + if (!state.draggable.initialCoordinates) { + return state; + } return { ...state, @@ -39,8 +39,7 @@ export function reducer(state: State, action: Actions): State { }, }, }; - case Action.DragEnd: - case Action.DragCancel: + case Action.ClearCoordinates: return { ...state, draggable: { diff --git a/packages/core/src/store/types.ts b/packages/core/src/store/types.ts index ea2acdff..5d8d44a6 100644 --- a/packages/core/src/store/types.ts +++ b/packages/core/src/store/types.ts @@ -67,7 +67,7 @@ export interface State { containers: DroppableContainers; }; draggable: { - initialCoordinates: Coordinates; + initialCoordinates: Coordinates | null; translate: Coordinates; }; } From ef3d7d839e93ab7b20454ee9a883ebc56127967d Mon Sep 17 00:00:00 2001 From: Alissa Date: Thu, 30 Mar 2023 07:48:48 +0300 Subject: [PATCH 05/32] add test for useDraggable for render only dragging element --- cypress/integration/draggable_spec.ts | 13 ++++++ cypress/support/commands.ts | 2 +- .../Draggable/3-MultipleDraggable.story.tsx | 43 ++++++++++++------- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/cypress/integration/draggable_spec.ts b/cypress/integration/draggable_spec.ts index c454ebdc..50f88273 100644 --- a/cypress/integration/draggable_spec.ts +++ b/cypress/integration/draggable_spec.ts @@ -416,4 +416,17 @@ describe('Draggable', () => { }); }); }); + + describe('Multiple Draggables', () => { + it('should render only dragging element', () => { + cy.visitStory('core-draggable-multi-draggable--basic-setup') + .findFirstDraggableItem() + + .mouseMoveBy(0, 100); + + cy.get('[data-testid="1"]').should('have.text', 'updated'); + cy.get('[data-testid="2"]').should('have.text', 'mounted'); + cy.get('[data-testid="3"]').should('have.text', 'mounted'); + }); + }); }); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 7036522d..daad03fe 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -27,7 +27,7 @@ function getDocumentScroll() { } Cypress.Commands.add('findFirstDraggableItem', () => { - return cy.get(`[data-cypress="draggable-item"`); + return cy.get(`[data-cypress="draggable-item"]`).first(); }); Cypress.Commands.add( diff --git a/stories/1 - Core/Draggable/3-MultipleDraggable.story.tsx b/stories/1 - Core/Draggable/3-MultipleDraggable.story.tsx index 7122a49b..568c821c 100644 --- a/stories/1 - Core/Draggable/3-MultipleDraggable.story.tsx +++ b/stories/1 - Core/Draggable/3-MultipleDraggable.story.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useState} from 'react'; +import React, {Profiler, useMemo, useRef, useState} from 'react'; import { DndContext, useDraggable, @@ -139,22 +139,35 @@ function DraggableItem({ useDraggable({ id: id, }); - - console.log('render draggable', id); + const span = useRef(null); return ( - + { + if (phase === 'update' && span.current) { + span.current.innerHTML = 'updated'; + } + }} + > +
+ + mounted + + +
+
); } From ca2b3dc66a2fb6fef3dcb0aa4a63f5008284cc51 Mon Sep 17 00:00:00 2001 From: Alissa Date: Thu, 30 Mar 2023 13:38:45 +0300 Subject: [PATCH 06/32] do not re-render draggable and droppabale when over changes --- .../src/components/DndContext/DndContext.tsx | 49 +++++++++---------- .../{activeAPI.ts => activeAndOverAPI.ts} | 25 +++++++++- packages/core/src/hooks/useDraggable.ts | 3 +- packages/core/src/hooks/useDroppable.ts | 11 +++-- packages/core/src/store/context.ts | 8 ++- packages/core/src/store/types.ts | 3 +- 6 files changed, 66 insertions(+), 33 deletions(-) rename packages/core/src/components/DndContext/{activeAPI.ts => activeAndOverAPI.ts} (74%) diff --git a/packages/core/src/components/DndContext/DndContext.tsx b/packages/core/src/components/DndContext/DndContext.tsx index 6856cfd2..7a92d93e 100644 --- a/packages/core/src/components/DndContext/DndContext.tsx +++ b/packages/core/src/components/DndContext/DndContext.tsx @@ -60,7 +60,7 @@ import { rectIntersection, } from '../../utilities'; import {applyModifiers, Modifiers} from '../../modifiers'; -import type {Active, Over} from '../../store/types'; +import type {Active} from '../../store/types'; import type { DragStartEvent, DragCancelEvent, @@ -82,7 +82,7 @@ import { useMeasuringConfiguration, } from './hooks'; import type {MeasuringConfiguration} from './types'; -import {createActiveAPI} from './activeAPI'; +import {createActiveAndOverAPI} from './activeAndOverAPI'; import {useActiveNodeDomValues} from './useActiveNodeDomValues'; export interface Props { @@ -157,12 +157,15 @@ export const DndContext = memo(function DndContext({ translated: null, }); - const activeAPI = useMemo(() => createActiveAPI(activeRects), []); - const draggableNodes = activeAPI.draggableNodes; - const active = activeAPI.useActive(); + const activeAndOverAPI = useMemo( + () => createActiveAndOverAPI(activeRects), + [] + ); + const draggableNodes = activeAndOverAPI.draggableNodes; + const active = activeAndOverAPI.useActive(); const activeId = active?.id || null; - const activatorEvent = activeAPI.useActivatorEvent(); + const activatorEvent = activeAndOverAPI.useActivatorEvent(); const activeRef = useRef(null); const [activeSensor, setActiveSensor] = useState(null); @@ -300,7 +303,7 @@ export const DndContext = memo(function DndContext({ }) : null; const overId = getFirstCollision(collisions, 'id'); - const [over, setOver] = useState(null); + const over = activeAndOverAPI.useOver(); // When there is no drag overlay used, we need to account for the // window scroll delta @@ -360,7 +363,7 @@ export const DndContext = memo(function DndContext({ unstable_batchedUpdates(() => { onDragStart?.(event); setStatus(Status.Initializing); - activeAPI.setActive(id); + activeAndOverAPI.setActive(id); dispatch({ type: Action.SetInitiailCoordinates, initialCoordinates, @@ -380,7 +383,7 @@ export const DndContext = memo(function DndContext({ unstable_batchedUpdates(() => { setActiveSensor(sensorInstance); - activeAPI.setActivatorEvent(event.nativeEvent); + activeAndOverAPI.setActivatorEvent(event.nativeEvent); }); function createHandler(type: 'DragEnd' | 'DragCancel') { @@ -412,12 +415,12 @@ export const DndContext = memo(function DndContext({ activeRef.current = null; unstable_batchedUpdates(() => { - activeAPI.setActive(null); + activeAndOverAPI.setActive(null); dispatch({type: Action.ClearCoordinates}); setStatus(Status.Uninitialized); - setOver(null); + activeAndOverAPI.setOver(null); setActiveSensor(null); - activeAPI.setActivatorEvent(null); + activeAndOverAPI.setActivatorEvent(null); const eventName = type === 'DragEnd' ? 'onDragEnd' : 'onDragCancel'; @@ -562,7 +565,7 @@ export const DndContext = memo(function DndContext({ }; unstable_batchedUpdates(() => { - setOver(over); + activeAndOverAPI.setOver(over); onDragOver?.(event); dispatchMonitorEvent({type: 'onDragOver', event}); }); @@ -660,11 +663,11 @@ export const DndContext = memo(function DndContext({ const internalContext = useMemo(() => { const context: InternalContextDescriptor = { - useMyActive: activeAPI.useMyActive, - useHasActive: activeAPI.useHasActive, - useGloablActive: activeAPI.useActive, - useMyActivatorEvent: activeAPI.useMyActivatorEvent, - useGlobalActivatorEvent: activeAPI.useActivatorEvent, + useMyActive: activeAndOverAPI.useMyActive, + useHasActive: activeAndOverAPI.useHasActive, + useGloablActive: activeAndOverAPI.useActive, + useMyActivatorEvent: activeAndOverAPI.useMyActivatorEvent, + useGlobalActivatorEvent: activeAndOverAPI.useActivatorEvent, useMyActiveNodeRect: (id: UniqueIdentifier) => { const domValues = useActiveNodeDomValues( draggableNodes, @@ -679,22 +682,18 @@ export const DndContext = memo(function DndContext({ }, dispatch, draggableNodes, - over, + useMyOverForDraggable: activeAndOverAPI.useMyOverForDraggable, + useMyOverForDroppable: activeAndOverAPI.useMyOverForDroppable, measureDroppableContainers, }; return context; }, [ - activeAPI.useMyActive, - activeAPI.useHasActive, - activeAPI.useActive, - activeAPI.useMyActivatorEvent, - activeAPI.useActivatorEvent, + activeAndOverAPI, activators, draggableDescribedById, dispatch, draggableNodes, - over, measureDroppableContainers, measuringConfiguration, ]); diff --git a/packages/core/src/components/DndContext/activeAPI.ts b/packages/core/src/components/DndContext/activeAndOverAPI.ts similarity index 74% rename from packages/core/src/components/DndContext/activeAPI.ts rename to packages/core/src/components/DndContext/activeAndOverAPI.ts index 11d6716d..129474a1 100644 --- a/packages/core/src/components/DndContext/activeAPI.ts +++ b/packages/core/src/components/DndContext/activeAndOverAPI.ts @@ -1,6 +1,6 @@ import type {MutableRefObject} from 'react'; import {useSyncExternalStore} from 'use-sync-external-store/shim'; -import type {Active} from '../../store'; +import type {Active, Over} from '../../store'; import type {UniqueIdentifier, ClientRect} from '../../types'; import {defaultData} from './defaults'; @@ -10,7 +10,7 @@ type Rects = MutableRefObject<{ translated: ClientRect | null; }>; -export function createActiveAPI(rect: Rects) { +export function createActiveAndOverAPI(rect: Rects) { let activeId: UniqueIdentifier | null = null; let active: Active | null = null; @@ -18,6 +18,8 @@ export function createActiveAPI(rect: Rects) { const draggableNodes = new Map(); const activeRects = rect; + let over: Over | null = null; + const registry: (() => void)[] = []; function subscribe(listener: () => void) { @@ -51,6 +53,11 @@ export function createActiveAPI(rect: Rects) { registry.forEach((li) => li()); }, + setOver: function (overInfo: Over | null) { + over = overInfo; + registry.forEach((li) => li()); + }, + useIsDragging: function (id: UniqueIdentifier) { return useSyncExternalStore(subscribe, () => activeId === id); }, @@ -76,5 +83,19 @@ export function createActiveAPI(rect: Rects) { activeId === id ? activatorEvent : null ); }, + + useMyOverForDraggable: function (draggableId: UniqueIdentifier) { + return useSyncExternalStore(subscribe, () => + activeId === draggableId ? over : null + ); + }, + useMyOverForDroppable: function (droppableId: UniqueIdentifier) { + return useSyncExternalStore(subscribe, () => + over && over.id === droppableId ? over : null + ); + }, + useOver: function () { + return useSyncExternalStore(subscribe, () => over); + }, }; } diff --git a/packages/core/src/hooks/useDraggable.ts b/packages/core/src/hooks/useDraggable.ts index ab887ebe..9be4ccb1 100644 --- a/packages/core/src/hooks/useDraggable.ts +++ b/packages/core/src/hooks/useDraggable.ts @@ -54,7 +54,7 @@ export function useDraggable({ useMyActiveNodeRect, ariaDescribedById, draggableNodes, - over, + useMyOverForDraggable, } = useContext(InternalContext); const { role = defaultRole, @@ -65,6 +65,7 @@ export function useDraggable({ const isDragging = active !== null; const activatorEvent = useMyActivatorEvent(id); const activeNodeRect = useMyActiveNodeRect(id); + const over = useMyOverForDraggable(id); const transform: Transform | null = useContext( isDragging ? ActiveDraggableContext : NullContext ); diff --git a/packages/core/src/hooks/useDroppable.ts b/packages/core/src/hooks/useDroppable.ts index 065f8bea..42478c35 100644 --- a/packages/core/src/hooks/useDroppable.ts +++ b/packages/core/src/hooks/useDroppable.ts @@ -43,9 +43,14 @@ export function useDroppable({ resizeObserverConfig, }: UseDroppableArguments) { const key = useUniqueId(ID_PREFIX); - const {useHasActive, dispatch, over, measureDroppableContainers} = - useContext(InternalContext); + const { + useHasActive, + dispatch, + useMyOverForDroppable, + measureDroppableContainers, + } = useContext(InternalContext); const hasActive = useHasActive(); + const over = useMyOverForDroppable(id); const previous = useRef({disabled}); const resizeObserverConnected = useRef(false); const rect = useRef(null); @@ -157,7 +162,7 @@ export function useDroppable({ return { //I removed the active from here, it forces all droppable to re-render when active changes. rect, - isOver: over?.id === id, + isOver: !!over, node: nodeRef, over, setNodeRef, diff --git a/packages/core/src/store/context.ts b/packages/core/src/store/context.ts index 50b1bb48..d0eb1c63 100644 --- a/packages/core/src/store/context.ts +++ b/packages/core/src/store/context.ts @@ -6,6 +6,7 @@ import {DroppableContainersMap} from './constructors'; import type { Active, InternalContextDescriptor, + Over, PublicContextDescriptor, } from './types'; import type {ClientRect} from '../types'; @@ -43,7 +44,6 @@ export const defaultInternalContext: InternalContextDescriptor = { }, dispatch: noop, draggableNodes: new Map(), - over: null, measureDroppableContainers: noop, useMyActive: function (): Active | null { throw new Error('Function not implemented.'); @@ -63,6 +63,12 @@ export const defaultInternalContext: InternalContextDescriptor = { useMyActiveNodeRect: function (): ClientRect | null { throw new Error('Function not implemented.'); }, + useMyOverForDraggable: function (): Over | null { + throw new Error('Function not implemented.'); + }, + useMyOverForDroppable: function (): Over | null { + throw new Error('Function not implemented.'); + }, }; export const InternalContext = createContext( diff --git a/packages/core/src/store/types.ts b/packages/core/src/store/types.ts index 5d8d44a6..237023d0 100644 --- a/packages/core/src/store/types.ts +++ b/packages/core/src/store/types.ts @@ -109,6 +109,7 @@ export interface InternalContextDescriptor { }; dispatch: React.Dispatch; draggableNodes: DraggableNodes; - over: Over | null; + useMyOverForDraggable: (draggableId: UniqueIdentifier) => Over | null; + useMyOverForDroppable: (droppableId: UniqueIdentifier) => Over | null; measureDroppableContainers(ids: UniqueIdentifier[]): void; } From 10ec00dc109cbbeeed3f16d3a1cc539965b47354 Mon Sep 17 00:00:00 2001 From: Alissa Date: Thu, 30 Mar 2023 14:11:40 +0300 Subject: [PATCH 07/32] remove dragging css class from droppable story, it did nothing and created and extra render --- stories/1 - Core/Droppable/Droppable.story.tsx | 6 +----- stories/components/Droppable/Droppable.module.css | 6 ------ stories/components/Droppable/Droppable.tsx | 4 +--- 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/stories/1 - Core/Droppable/Droppable.story.tsx b/stories/1 - Core/Droppable/Droppable.story.tsx index b6f6f424..ec4e1b82 100644 --- a/stories/1 - Core/Droppable/Droppable.story.tsx +++ b/stories/1 - Core/Droppable/Droppable.story.tsx @@ -35,7 +35,6 @@ function DroppableStory({ collisionDetection, modifiers, }: Props) { - const [isDragging, setIsDragging] = useState(false); const [parent, setParent] = useState(null); const item = ; @@ -44,12 +43,9 @@ function DroppableStory({ setIsDragging(true)} onDragEnd={({over}) => { setParent(over ? over.id : null); - setIsDragging(false); }} - onDragCancel={() => setIsDragging(false)} > @@ -57,7 +53,7 @@ function DroppableStory({ {containers.map((id) => ( - + {parent === id ? item : null} ))} diff --git a/stories/components/Droppable/Droppable.module.css b/stories/components/Droppable/Droppable.module.css index 70e77c9c..7159736b 100644 --- a/stories/components/Droppable/Droppable.module.css +++ b/stories/components/Droppable/Droppable.module.css @@ -23,12 +23,6 @@ pointer-events: none; } - &.dragging { - > svg { - opacity: 0.8; - } - } - &.over { box-shadow: inset #1eb99d 0 0 0 3px, rgba(201, 211, 219, 0.5) 20px 14px 24px; diff --git a/stories/components/Droppable/Droppable.tsx b/stories/components/Droppable/Droppable.tsx index e3f480a2..afdf13d6 100644 --- a/stories/components/Droppable/Droppable.tsx +++ b/stories/components/Droppable/Droppable.tsx @@ -7,11 +7,10 @@ import styles from './Droppable.module.css'; interface Props { children: React.ReactNode; - dragging: boolean; id: UniqueIdentifier; } -export function Droppable({children, id, dragging}: Props) { +export function Droppable({children, id}: Props) { const {isOver, setNodeRef} = useDroppable({ id, }); @@ -22,7 +21,6 @@ export function Droppable({children, id, dragging}: Props) { className={classNames( styles.Droppable, isOver && styles.over, - dragging && styles.dragging, children && styles.dropped )} aria-label="Droppable region" From acdfb555c928bf6b6386b2f1d7b75bc2883d44f0 Mon Sep 17 00:00:00 2001 From: Alissa Date: Thu, 30 Mar 2023 14:14:53 +0300 Subject: [PATCH 08/32] remove hasActive from useDroppable --- .../core/src/components/DndContext/DndContext.tsx | 1 - .../src/components/DndContext/activeAndOverAPI.ts | 3 --- packages/core/src/hooks/useDroppable.ts | 13 +++++-------- packages/core/src/store/context.ts | 3 --- packages/core/src/store/types.ts | 1 - 5 files changed, 5 insertions(+), 16 deletions(-) diff --git a/packages/core/src/components/DndContext/DndContext.tsx b/packages/core/src/components/DndContext/DndContext.tsx index 7a92d93e..edf29a76 100644 --- a/packages/core/src/components/DndContext/DndContext.tsx +++ b/packages/core/src/components/DndContext/DndContext.tsx @@ -664,7 +664,6 @@ export const DndContext = memo(function DndContext({ const internalContext = useMemo(() => { const context: InternalContextDescriptor = { useMyActive: activeAndOverAPI.useMyActive, - useHasActive: activeAndOverAPI.useHasActive, useGloablActive: activeAndOverAPI.useActive, useMyActivatorEvent: activeAndOverAPI.useMyActivatorEvent, useGlobalActivatorEvent: activeAndOverAPI.useActivatorEvent, diff --git a/packages/core/src/components/DndContext/activeAndOverAPI.ts b/packages/core/src/components/DndContext/activeAndOverAPI.ts index 129474a1..ead8695b 100644 --- a/packages/core/src/components/DndContext/activeAndOverAPI.ts +++ b/packages/core/src/components/DndContext/activeAndOverAPI.ts @@ -62,9 +62,6 @@ export function createActiveAndOverAPI(rect: Rects) { return useSyncExternalStore(subscribe, () => activeId === id); }, - useHasActive: function () { - return useSyncExternalStore(subscribe, () => activeId !== null); - }, useActive: function () { return useSyncExternalStore(subscribe, () => active); }, diff --git a/packages/core/src/hooks/useDroppable.ts b/packages/core/src/hooks/useDroppable.ts index 42478c35..05a1366a 100644 --- a/packages/core/src/hooks/useDroppable.ts +++ b/packages/core/src/hooks/useDroppable.ts @@ -43,13 +43,8 @@ export function useDroppable({ resizeObserverConfig, }: UseDroppableArguments) { const key = useUniqueId(ID_PREFIX); - const { - useHasActive, - dispatch, - useMyOverForDroppable, - measureDroppableContainers, - } = useContext(InternalContext); - const hasActive = useHasActive(); + const {dispatch, useMyOverForDroppable, measureDroppableContainers} = + useContext(InternalContext); const over = useMyOverForDroppable(id); const previous = useRef({disabled}); const resizeObserverConnected = useRef(false); @@ -89,7 +84,9 @@ export function useDroppable({ ); const resizeObserver = useResizeObserver({ callback: handleResize, - disabled: resizeObserverDisabled || !hasActive, + //the use of hasActive here forces all droppable to re-render when start/end drag. + //are we sure it is needed to disable the resize observer when there is no active drag? + disabled: resizeObserverDisabled, }); const handleNodeChange = useCallback( (newElement: HTMLElement | null, previousElement: HTMLElement | null) => { diff --git a/packages/core/src/store/context.ts b/packages/core/src/store/context.ts index d0eb1c63..6ad84a72 100644 --- a/packages/core/src/store/context.ts +++ b/packages/core/src/store/context.ts @@ -51,9 +51,6 @@ export const defaultInternalContext: InternalContextDescriptor = { useGloablActive: function (): Active | null { throw new Error('Function not implemented.'); }, - useHasActive: function (): boolean { - throw new Error('Function not implemented.'); - }, useMyActivatorEvent: function (): Event | null { throw new Error('Function not implemented.'); }, diff --git a/packages/core/src/store/types.ts b/packages/core/src/store/types.ts index 237023d0..e1603b1e 100644 --- a/packages/core/src/store/types.ts +++ b/packages/core/src/store/types.ts @@ -100,7 +100,6 @@ export interface InternalContextDescriptor { activators: SyntheticListeners; useMyActive: (id: UniqueIdentifier) => Active | null; useGloablActive: () => Active | null; - useHasActive: () => boolean; useMyActivatorEvent: (id: UniqueIdentifier) => Event | null; useGlobalActivatorEvent: () => Event | null; useMyActiveNodeRect: (id: UniqueIdentifier) => ClientRect | null; From 18a7ce56faf6140800c464902178d9c9e39925e4 Mon Sep 17 00:00:00 2001 From: Alissa Date: Thu, 30 Mar 2023 20:31:25 +0300 Subject: [PATCH 09/32] add test for render only relevant draggable element and container --- cypress/integration/draggable_spec.ts | 17 ++- cypress/integration/droppable_spec.ts | 42 ++++++ cypress/support/commands.ts | 56 ++++--- cypress/support/index.d.ts | 2 +- ...e.story.tsx => DraggableRenders.story.tsx} | 4 +- .../Droppable/DroppableRenders.story.tsx | 141 ++++++++++++++++++ stories/components/Droppable/Droppable.tsx | 30 +++- 7 files changed, 258 insertions(+), 34 deletions(-) create mode 100644 cypress/integration/droppable_spec.ts rename stories/1 - Core/Draggable/{3-MultipleDraggable.story.tsx => DraggableRenders.story.tsx} (97%) create mode 100644 stories/1 - Core/Droppable/DroppableRenders.story.tsx diff --git a/cypress/integration/draggable_spec.ts b/cypress/integration/draggable_spec.ts index 50f88273..737b49a8 100644 --- a/cypress/integration/draggable_spec.ts +++ b/cypress/integration/draggable_spec.ts @@ -419,14 +419,23 @@ describe('Draggable', () => { describe('Multiple Draggables', () => { it('should render only dragging element', () => { - cy.visitStory('core-draggable-multi-draggable--basic-setup') + cy.visitStory('core-draggable-draggablerenders--basic-setup') .findFirstDraggableItem() .mouseMoveBy(0, 100); - cy.get('[data-testid="1"]').should('have.text', 'updated'); - cy.get('[data-testid="2"]').should('have.text', 'mounted'); - cy.get('[data-testid="3"]').should('have.text', 'mounted'); + cy.get('[data-testid="draggable-status-1"]').should( + 'have.text', + 'updated' + ); + cy.get('[data-testid="draggable-status-2"]').should( + 'have.text', + 'mounted' + ); + cy.get('[data-testid="draggable-status-3"]').should( + 'have.text', + 'mounted' + ); }); }); }); diff --git a/cypress/integration/droppable_spec.ts b/cypress/integration/droppable_spec.ts new file mode 100644 index 00000000..4c6dedf1 --- /dev/null +++ b/cypress/integration/droppable_spec.ts @@ -0,0 +1,42 @@ +/// + +describe('Droppable', () => { + describe('Droppable Renders', () => { + it('should re-render only the dragged item and the container dragged over', () => { + cy.visitStory('core-droppablerenders-usedroppable--multiple-droppables'); + + cy.get('[data-cypress="droppable-container-C"]').then((droppable) => { + const coords = droppable[0].getBoundingClientRect(); + return cy + .findFirstDraggableItem() + .mouseMoveBy(coords.x + 10, coords.y + 10, {delay: 1, noDrop: true}); + }); + + cy.get('[data-testid="draggable-status-1"]').should( + 'have.text', + 'updated' + ); + cy.get('[data-testid="draggable-status-2"]').should( + 'have.text', + 'mounted' + ); + cy.get('[data-testid="draggable-status-3"]').should( + 'have.text', + 'mounted' + ); + + cy.get('[data-testid="droppable-status-A"]').should( + 'have.text', + 'mounted' + ); + cy.get('[data-testid="droppable-status-B"]').should( + 'have.text', + 'mounted' + ); + cy.get('[data-testid="droppable-status-C"]').should( + 'have.text', + 'updated' + ); + }); + }); +}); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index daad03fe..55673593 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -47,7 +47,12 @@ Cypress.Commands.add( { prevSubject: 'element', }, - (subject, x: number, y: number, options?: {delay: number}) => { + ( + subject, + x: number, + y: number, + options?: {delay: number; noDrop?: boolean} + ) => { cy.wrap(subject, {log: false}) .then((subject) => { const initialRect = subject.get(0).getBoundingClientRect(); @@ -56,7 +61,8 @@ Cypress.Commands.add( return [subject, initialRect, windowScroll] as const; }) .then(([subject, initialRect, initialWindowScroll]) => { - cy.wrap(subject) + let resultOps = cy + .wrap(subject) .trigger('mousedown', {force: true}) .wait(options?.delay || 0, {log: Boolean(options?.delay)}) .trigger('mousemove', { @@ -72,29 +78,31 @@ Cypress.Commands.add( force: true, clientX: Math.floor(initialRect.left + initialRect.width / 2 + x), clientY: Math.floor(initialRect.top + initialRect.height / 2 + y), - }) - .wait(100) - .trigger('mouseup', {force: true}) - .wait(250) - .then((subject: any) => { - const finalRect = subject.get(0).getBoundingClientRect(); - const windowScroll = getDocumentScroll(); - const windowScrollDelta = { - x: windowScroll.x - initialWindowScroll.x, - y: windowScroll.y - initialWindowScroll.y, - }; - - const delta = { - x: Math.round( - finalRect.left - initialRect.left - windowScrollDelta.x - ), - y: Math.round( - finalRect.top - initialRect.top - windowScrollDelta.y - ), - }; - - return [subject, {initialRect, finalRect, delta}] as const; }); + + if (!options?.noDrop) { + resultOps = resultOps.wait(100).trigger('mouseup', {force: true}); + } + + resultOps.wait(250).then((subject: any) => { + const finalRect = subject.get(0).getBoundingClientRect(); + const windowScroll = getDocumentScroll(); + const windowScrollDelta = { + x: windowScroll.x - initialWindowScroll.x, + y: windowScroll.y - initialWindowScroll.y, + }; + + const delta = { + x: Math.round( + finalRect.left - initialRect.left - windowScrollDelta.x + ), + y: Math.round( + finalRect.top - initialRect.top - windowScrollDelta.y + ), + }; + + return [subject, {initialRect, finalRect, delta}] as const; + }); }); } ); diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts index 5ded7e3d..9329c491 100644 --- a/cypress/support/index.d.ts +++ b/cypress/support/index.d.ts @@ -23,7 +23,7 @@ declare namespace Cypress { mouseMoveBy( x: number, y: number, - options?: {delay: number} + options?: {delay: number; noDrop?: boolean} ): Chainable< [ Element, diff --git a/stories/1 - Core/Draggable/3-MultipleDraggable.story.tsx b/stories/1 - Core/Draggable/DraggableRenders.story.tsx similarity index 97% rename from stories/1 - Core/Draggable/3-MultipleDraggable.story.tsx rename to stories/1 - Core/Draggable/DraggableRenders.story.tsx index 568c821c..5e8c1a78 100644 --- a/stories/1 - Core/Draggable/3-MultipleDraggable.story.tsx +++ b/stories/1 - Core/Draggable/DraggableRenders.story.tsx @@ -16,7 +16,7 @@ import type {Coordinates} from '@dnd-kit/utilities'; import {Axis, Draggable, Wrapper} from '../../components'; export default { - title: 'Core/Draggable/Multi/draggable', + title: 'Core/Draggable/DraggableRenders', }; const defaultCoordinates = { @@ -151,7 +151,7 @@ function DraggableItem({ }} >
- + mounted ({}); + const orphanItems = useMemo( + () => items.filter((itemId) => !parents[itemId]), + [items, parents] + ); + const itemsPyParent = useMemo(() => { + return Object.entries(parents).reduce((acc, [itemId, parentId]) => { + acc[parentId] = acc[parentId] || []; + acc[parentId].push(itemId); + return acc; + }, {} as {[parentId: UniqueIdentifier]: UniqueIdentifier[]}); + }, [parents]); + + const mouseSensor = useSensor(MouseSensor); + const sensors = useSensors(mouseSensor); + return ( + { + if ((!over && !parents[active.id]) || over?.id === parents[active.id]) { + return; + } + if (over) { + setParents((prev) => ({...prev, [active.id]: over.id})); + } else { + setParents((prev) => { + const {[active.id]: _, ...rest} = prev; + return rest; + }); + } + }} + > + + + {orphanItems.map((itemId) => ( + + ))} + + + {containers.map((id) => ( + + {itemsPyParent[id]?.map((itemId) => ( + + )) || null} + + ))} + + + + + ); +} + +interface DraggableProps { + handle?: boolean; + id: UniqueIdentifier; +} + +function DraggableItem({handle, id}: DraggableProps) { + const {isDragging, setNodeRef, listeners, attributes, transform} = + useDraggable({ + id: id, + }); + + const span = useRef(null); + + return ( + { + if (phase === 'update' && span.current) { + span.current.innerHTML = 'updated'; + } + }} + > +
+ + mounted + + +
+
+ ); +} + +export const MultipleDroppables = () => ( + +); diff --git a/stories/components/Droppable/Droppable.tsx b/stories/components/Droppable/Droppable.tsx index afdf13d6..94d906cc 100644 --- a/stories/components/Droppable/Droppable.tsx +++ b/stories/components/Droppable/Droppable.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {Profiler, useRef} from 'react'; import {useDroppable, UniqueIdentifier} from '@dnd-kit/core'; import classNames from 'classnames'; @@ -8,16 +8,20 @@ import styles from './Droppable.module.css'; interface Props { children: React.ReactNode; id: UniqueIdentifier; + showRenderState?: boolean; } -export function Droppable({children, id}: Props) { +export function Droppable({children, id, showRenderState}: Props) { const {isOver, setNodeRef} = useDroppable({ id, }); - return ( + const span = useRef(null); + + const DroppableContent = (
); + + return showRenderState ? ( + { + if (phase === 'update' && span.current) { + span.current.innerHTML = 'updated'; + } + }} + > +
+ + mounted + + {DroppableContent} +
+
+ ) : ( + DroppableContent + ); } From 618d22b9ae1ec5c4a55eb7684262cac0f325d61e Mon Sep 17 00:00:00 2001 From: Alissa Date: Fri, 31 Mar 2023 19:45:49 +0300 Subject: [PATCH 10/32] sortable renders story --- stories/2 - Presets/Sortable/Sortable.tsx | 116 ++++++++++-------- .../Sortable/SortableRenders.story.tsx | 96 +++++++++++++++ 2 files changed, 160 insertions(+), 52 deletions(-) create mode 100644 stories/2 - Presets/Sortable/SortableRenders.story.tsx diff --git a/stories/2 - Presets/Sortable/Sortable.tsx b/stories/2 - Presets/Sortable/Sortable.tsx index b96a13f5..6272c5ae 100644 --- a/stories/2 - Presets/Sortable/Sortable.tsx +++ b/stories/2 - Presets/Sortable/Sortable.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useRef, useState} from 'react'; +import React, {useRef, useState} from 'react'; import {createPortal} from 'react-dom'; import { @@ -21,6 +21,7 @@ import { useSensor, useSensors, defaultDropAnimationSideEffects, + useDndContext, } from '@dnd-kit/core'; import { arrayMove, @@ -120,7 +121,7 @@ export function Sortable({ initialItems ?? createRange(itemCount, (index) => index + 1) ); - const [activeId, setActiveId] = useState(null); + const sensors = useSensors( useSensor(MouseSensor, { activationConstraint, @@ -137,7 +138,6 @@ export function Sortable({ const isFirstAnnouncement = useRef(true); const getIndex = (id: UniqueIdentifier) => items.indexOf(id); const getPosition = (id: UniqueIdentifier) => getIndex(id) + 1; - const activeIndex = activeId ? getIndex(activeId) : -1; const handleRemove = removable ? (id: UniqueIdentifier) => setItems((items) => items.filter((item) => item !== id)) @@ -168,6 +168,7 @@ export function Sortable({ return; }, onDragEnd({active, over}) { + isFirstAnnouncement.current = true; if (over) { return `Sortable item ${ active.id @@ -177,18 +178,13 @@ export function Sortable({ return; }, onDragCancel({active: {id}}) { + isFirstAnnouncement.current = true; return `Sorting was cancelled. Sortable item ${id} was dropped and returned to position ${getPosition( id )} of ${items.length}.`; }, }; - useEffect(() => { - if (!activeId) { - isFirstAnnouncement.current = true; - } - }, [activeId]); - return ( { - if (!active) { - return; - } - - setActiveId(active.id); - }} - onDragEnd={({over}) => { - setActiveId(null); - + onDragEnd={({over, active}) => { if (over) { const overIndex = getIndex(over.id); + const activeIndex = getIndex(active.id); if (activeIndex !== overIndex) { setItems((items) => reorderItems(items, activeIndex, overIndex)); } } }} - onDragCancel={() => setActiveId(null)} measuring={measuring} modifiers={modifiers} > @@ -240,42 +227,67 @@ export function Sortable({ - {useDragOverlay - ? createPortal( - - {activeId ? ( - - ) : null} - , - document.body - ) - : null} + {useDragOverlay ? ( + + ) : null} ); } +function SortableDragOverlay({ + items, + adjustScale, + dropAnimation, + handle, + renderItem, + wrapperStyle, + getItemStyles, +}: Pick & { + items: UniqueIdentifier[]; + wrapperStyle: Exclude; + getItemStyles: Exclude; +}) { + const {active} = useDndContext(); + const getIndex = (id: UniqueIdentifier) => items.indexOf(id); + const activeIndex = active ? getIndex(active.id) : -1; + + return createPortal( + + {active ? ( + + ) : null} + , + document.body + ); +} + interface SortableItemProps { animateLayoutChanges?: AnimateLayoutChanges; disabled?: boolean; diff --git a/stories/2 - Presets/Sortable/SortableRenders.story.tsx b/stories/2 - Presets/Sortable/SortableRenders.story.tsx new file mode 100644 index 00000000..1246ed2e --- /dev/null +++ b/stories/2 - Presets/Sortable/SortableRenders.story.tsx @@ -0,0 +1,96 @@ +import React, {Profiler, useRef, useState} from 'react'; +import {arrayMove, SortableContext, useSortable} from '@dnd-kit/sortable'; + +import {Container, Item, Wrapper} from '../../components'; +import { + DndContext, + MouseSensor, + UniqueIdentifier, + useSensor, + useSensors, +} from '@dnd-kit/core'; +import {createRange} from '../../utilities'; + +export default { + title: 'Presets/Sortable/Renders', +}; + +function SortableItem({id, index}: {id: UniqueIdentifier; index: number}) { + const { + attributes, + isDragging, + isSorting, + listeners, + setNodeRef, + transform, + transition, + } = useSortable({ + id, + }); + + const span = useRef(null); + + return ( + { + if (phase === 'update' && span.current) { + span.current.innerHTML = 'updated ' + id; + } + }} + > +
+ + mounted {id} + + +
+
+ ); +} + +function Sortable() { + const [items, setItems] = useState(() => + createRange(20, (index) => index + 1) + ); + const getIndex = (id: UniqueIdentifier) => items.indexOf(id); + const sensors = useSensors(useSensor(MouseSensor)); + return ( + { + if (over) { + const overIndex = getIndex(over.id); + const activeIndex = getIndex(active.id); + if (activeIndex !== overIndex) { + setItems((items) => arrayMove(items, activeIndex, overIndex)); + } + } + }} + > + + + + {items.map((id, index) => ( + + ))} + + + + + ); +} + +export const BasicSetup = () => ; From 83207fba23c83d6c71f854caf857182691633c8c Mon Sep 17 00:00:00 2001 From: Alissa Date: Fri, 31 Mar 2023 21:57:35 +0300 Subject: [PATCH 11/32] runs, not rendering all --- .../src/components/DndContext/DndContext.tsx | 2 + .../components/DndContext/activeAndOverAPI.ts | 4 + packages/core/src/store/types.ts | 2 + .../src/components/SortableContext.tsx | 67 ++++++--- .../sortable/src/components/sortingAPI.tsx | 47 +++++++ .../src/components/usePreviousSortingState.ts | 42 ++++++ packages/sortable/src/hooks/defaults.ts | 11 +- packages/sortable/src/hooks/index.ts | 2 +- packages/sortable/src/hooks/types.ts | 9 -- packages/sortable/src/hooks/useSortable.ts | 133 +++++++----------- packages/sortable/src/index.ts | 8 +- packages/sortable/src/types/index.ts | 1 + packages/sortable/src/types/indexGetter.ts | 10 ++ 13 files changed, 216 insertions(+), 122 deletions(-) create mode 100644 packages/sortable/src/components/sortingAPI.tsx create mode 100644 packages/sortable/src/components/usePreviousSortingState.ts create mode 100644 packages/sortable/src/types/indexGetter.ts diff --git a/packages/core/src/components/DndContext/DndContext.tsx b/packages/core/src/components/DndContext/DndContext.tsx index edf29a76..63b8779a 100644 --- a/packages/core/src/components/DndContext/DndContext.tsx +++ b/packages/core/src/components/DndContext/DndContext.tsx @@ -638,6 +638,7 @@ export const DndContext = memo(function DndContext({ measuringConfiguration, measuringScheduled, windowRect, + activeAndOverAPI: activeAndOverAPI, }; return context; @@ -659,6 +660,7 @@ export const DndContext = memo(function DndContext({ measuringConfiguration, measuringScheduled, windowRect, + activeAndOverAPI, ]); const internalContext = useMemo(() => { diff --git a/packages/core/src/components/DndContext/activeAndOverAPI.ts b/packages/core/src/components/DndContext/activeAndOverAPI.ts index ead8695b..9e295c86 100644 --- a/packages/core/src/components/DndContext/activeAndOverAPI.ts +++ b/packages/core/src/components/DndContext/activeAndOverAPI.ts @@ -41,6 +41,7 @@ export function createActiveAndOverAPI(rect: Rects) { return { draggableNodes, + subscribe, setActive: function (id: UniqueIdentifier | null) { if (activeId === id) return; activeId = id; @@ -58,6 +59,9 @@ export function createActiveAndOverAPI(rect: Rects) { registry.forEach((li) => li()); }, + getActive: () => active, + getOver: () => over, + useIsDragging: function (id: UniqueIdentifier) { return useSyncExternalStore(subscribe, () => activeId === id); }, diff --git a/packages/core/src/store/types.ts b/packages/core/src/store/types.ts index e1603b1e..ed9cbb28 100644 --- a/packages/core/src/store/types.ts +++ b/packages/core/src/store/types.ts @@ -8,6 +8,7 @@ import type {Coordinates, ClientRect, UniqueIdentifier} from '../types'; import type {Actions} from './actions'; import type {DroppableContainersMap} from './constructors'; +import type {createActiveAndOverAPI} from '../components/DndContext/activeAndOverAPI'; export interface DraggableElement { node: DraggableNode; @@ -94,6 +95,7 @@ export interface PublicContextDescriptor { measureDroppableContainers(ids: UniqueIdentifier[]): void; measuringScheduled: boolean; windowRect: ClientRect | null; + activeAndOverAPI: ReturnType; } export interface InternalContextDescriptor { diff --git a/packages/sortable/src/components/SortableContext.tsx b/packages/sortable/src/components/SortableContext.tsx index ed370e24..da551505 100644 --- a/packages/sortable/src/components/SortableContext.tsx +++ b/packages/sortable/src/components/SortableContext.tsx @@ -2,9 +2,12 @@ import React, {useEffect, useMemo, useRef} from 'react'; import {useDndContext, ClientRect, UniqueIdentifier} from '@dnd-kit/core'; import {useIsomorphicLayoutEffect, useUniqueId} from '@dnd-kit/utilities'; -import type {Disabled, SortingStrategy} from '../types'; +import type {Disabled, NewIndexGetter, SortingStrategy} from '../types'; import {getSortedRects, itemsEqual, normalizeDisabled} from '../utilities'; import {rectSortingStrategy} from '../strategies'; +import {createSortingAPI} from './sortingAPI'; +import {usePreviousSortingStateRef} from './usePreviousSortingState'; +import {defaultNewIndexGetter} from '../hooks/defaults'; export interface Props { children: React.ReactNode; @@ -12,35 +15,42 @@ export interface Props { strategy?: SortingStrategy; id?: string; disabled?: boolean | Disabled; + getNewIndex?: NewIndexGetter; } const ID_PREFIX = 'Sortable'; interface ContextDescriptor { - activeIndex: number; containerId: string; disabled: Disabled; disableTransforms: boolean; items: UniqueIdentifier[]; - overIndex: number; useDragOverlay: boolean; - sortedRects: ClientRect[]; strategy: SortingStrategy; + useMyNewIndex: (id: UniqueIdentifier, currentIndex: number) => number; + previousSortingStateRef: ReturnType; } export const Context = React.createContext({ - activeIndex: -1, containerId: ID_PREFIX, disableTransforms: false, items: [], - overIndex: -1, useDragOverlay: false, - sortedRects: [], strategy: rectSortingStrategy, disabled: { draggable: false, droppable: false, }, + useMyNewIndex: () => -1, + previousSortingStateRef: { + current: {activeId: null, containerId: '', items: []}, + }, +}); + +export const ActiveContext = React.createContext({ + activeIndex: -1, + overIndex: -1, + sortedRects: [] as ClientRect[], }); export function SortableContext({ @@ -49,13 +59,14 @@ export function SortableContext({ items: userDefinedItems, strategy = rectSortingStrategy, disabled: disabledProp = false, + getNewIndex = defaultNewIndexGetter, }: Props) { const { active, dragOverlay, droppableRects, - over, measureDroppableContainers, + activeAndOverAPI, } = useDndContext(); const containerId = useUniqueId(ID_PREFIX, id); const useDragOverlay = Boolean(dragOverlay.rect !== null); @@ -66,9 +77,16 @@ export function SortableContext({ ), [userDefinedItems] ); + const sortingAPI = useMemo( + () => createSortingAPI(items, activeAndOverAPI, getNewIndex), + [activeAndOverAPI, items, getNewIndex] + ); + useEffect(() => { + return sortingAPI.clear; + }); const isDragging = active != null; - const activeIndex = active ? items.indexOf(active.id) : -1; - const overIndex = over ? items.indexOf(over.id) : -1; + const activeIndex = sortingAPI.getActiveIndex(); + const overIndex = sortingAPI.getOverIndex(); const previousItemsRef = useRef(items); const itemsHaveChanged = !itemsEqual(items, previousItemsRef.current); const disableTransforms = @@ -85,32 +103,47 @@ export function SortableContext({ previousItemsRef.current = items; }, [items]); + const activeContextValue = useMemo( + () => ({ + activeIndex, + overIndex, + sortedRects: getSortedRects(items, droppableRects), + }), + [activeIndex, droppableRects, items, overIndex] + ); + const previousSortingStateRef = usePreviousSortingStateRef({ + activeId: active?.id || null, + containerId, + items, + }); const contextValue = useMemo( (): ContextDescriptor => ({ - activeIndex, containerId, disabled, disableTransforms, items, - overIndex, useDragOverlay, - sortedRects: getSortedRects(items, droppableRects), strategy, + useMyNewIndex: sortingAPI.useMyNewIndex, + previousSortingStateRef, }), // eslint-disable-next-line react-hooks/exhaustive-deps [ - activeIndex, containerId, disabled.draggable, disabled.droppable, disableTransforms, items, - overIndex, - droppableRects, useDragOverlay, strategy, ] ); - return {children}; + return ( + + + {children} + + + ); } diff --git a/packages/sortable/src/components/sortingAPI.tsx b/packages/sortable/src/components/sortingAPI.tsx new file mode 100644 index 00000000..413e2ac8 --- /dev/null +++ b/packages/sortable/src/components/sortingAPI.tsx @@ -0,0 +1,47 @@ +import type {UniqueIdentifier, DndContextDescriptor} from '@dnd-kit/core'; +import {useSyncExternalStore} from 'use-sync-external-store'; +import {defaultNewIndexGetter} from '../hooks/defaults'; +import {isValidIndex} from '../utilities'; + +export function createSortingAPI( + items: UniqueIdentifier[], + activeAndOverAPI: DndContextDescriptor['activeAndOverAPI'], + getNewIndex = defaultNewIndexGetter +) { + let activeIndex: number = -1; + let overIndex: number = -1; + calculateIndexes(); + + const registry: (() => void)[] = []; + + function subscribe(listener: () => void) { + registry.push(listener); + return () => { + registry.splice(registry.indexOf(listener), 1); + }; + } + + function calculateIndexes() { + const active = activeAndOverAPI.getActive(); + activeIndex = active ? items.indexOf(active.id) : -1; + const over = activeAndOverAPI.getOver(); + overIndex = over ? items.indexOf(over.id) : -1; + } + + const unsubscribeFromActiveAndOver = activeAndOverAPI.subscribe(() => { + registry.forEach((li) => li()); + }); + + return { + clear: unsubscribeFromActiveAndOver, + useMyNewIndex: (id: UniqueIdentifier, currentIndex: number) => { + return useSyncExternalStore(subscribe, () => { + return isValidIndex(activeIndex) && isValidIndex(overIndex) + ? getNewIndex({id, items, activeIndex, overIndex}) + : currentIndex; + }); + }, + getActiveIndex: () => activeIndex, + getOverIndex: () => overIndex, + }; +} diff --git a/packages/sortable/src/components/usePreviousSortingState.ts b/packages/sortable/src/components/usePreviousSortingState.ts new file mode 100644 index 00000000..54150b30 --- /dev/null +++ b/packages/sortable/src/components/usePreviousSortingState.ts @@ -0,0 +1,42 @@ +import type {UniqueIdentifier} from '@dnd-kit/core'; +import {useRef, useEffect} from 'react'; +export type SortableState = { + activeId: UniqueIdentifier | null; + items: UniqueIdentifier[]; + containerId: string; +}; + +export function usePreviousSortingStateRef(sortingState: SortableState) { + const previous = useRef(sortingState); + + const activeId = sortingState.activeId; + useEffect(() => { + if (activeId === previous.current.activeId) { + return; + } + + if (activeId && !previous.current.activeId) { + previous.current.activeId = activeId; + return; + } + + const timeoutId = setTimeout(() => { + previous.current.activeId = activeId; + }, 50); + + return () => clearTimeout(timeoutId); + }, [activeId]); + + const {items, containerId} = sortingState; + useEffect(() => { + if (containerId !== previous.current.containerId) { + previous.current.containerId = containerId; + } + + if (items !== previous.current.items) { + previous.current.items = items; + } + }, [activeId, containerId, items]); + + return previous; +} diff --git a/packages/sortable/src/hooks/defaults.ts b/packages/sortable/src/hooks/defaults.ts index c0cf1e0a..bb1df92a 100644 --- a/packages/sortable/src/hooks/defaults.ts +++ b/packages/sortable/src/hooks/defaults.ts @@ -1,12 +1,9 @@ import {CSS} from '@dnd-kit/utilities'; +import type {NewIndexGetter} from '../types'; import {arrayMove} from '../utilities'; -import type { - AnimateLayoutChanges, - NewIndexGetter, - SortableTransition, -} from './types'; +import type {AnimateLayoutChanges, SortableTransition} from './types'; export const defaultNewIndexGetter: NewIndexGetter = ({ id, @@ -17,7 +14,7 @@ export const defaultNewIndexGetter: NewIndexGetter = ({ export const defaultAnimateLayoutChanges: AnimateLayoutChanges = ({ containerId, - isSorting, + isDragging, wasDragging, index, items, @@ -34,7 +31,7 @@ export const defaultAnimateLayoutChanges: AnimateLayoutChanges = ({ return false; } - if (isSorting) { + if (isDragging) { return true; } diff --git a/packages/sortable/src/hooks/index.ts b/packages/sortable/src/hooks/index.ts index a00168c9..c55551c2 100644 --- a/packages/sortable/src/hooks/index.ts +++ b/packages/sortable/src/hooks/index.ts @@ -2,4 +2,4 @@ export {useSortable} from './useSortable'; export type {Arguments as UseSortableArguments} from './useSortable'; export {defaultAnimateLayoutChanges, defaultNewIndexGetter} from './defaults'; -export type {AnimateLayoutChanges, NewIndexGetter} from './types'; +export type {AnimateLayoutChanges} from './types'; diff --git a/packages/sortable/src/hooks/types.ts b/packages/sortable/src/hooks/types.ts index 26c67d24..c7cbfc5f 100644 --- a/packages/sortable/src/hooks/types.ts +++ b/packages/sortable/src/hooks/types.ts @@ -17,12 +17,3 @@ export type AnimateLayoutChanges = (args: { transition: SortableTransition | null; wasDragging: boolean; }) => boolean; - -export interface NewIndexGetterArguments { - id: UniqueIdentifier; - items: UniqueIdentifier[]; - activeIndex: number; - overIndex: number; -} - -export type NewIndexGetter = (args: NewIndexGetterArguments) => number; diff --git a/packages/sortable/src/hooks/useSortable.ts b/packages/sortable/src/hooks/useSortable.ts index 6560b8a6..074a4f21 100644 --- a/packages/sortable/src/hooks/useSortable.ts +++ b/packages/sortable/src/hooks/useSortable.ts @@ -1,4 +1,4 @@ -import {useContext, useEffect, useMemo, useRef} from 'react'; +import {createContext, useContext, useEffect, useMemo, useRef} from 'react'; import { useDraggable, useDroppable, @@ -14,24 +14,20 @@ import {isValidIndex} from '../utilities'; import { defaultAnimateLayoutChanges, defaultAttributes, - defaultNewIndexGetter, defaultTransition, disabledTransition, transitionProperty, } from './defaults'; -import type { - AnimateLayoutChanges, - NewIndexGetter, - SortableTransition, -} from './types'; +import type {AnimateLayoutChanges, SortableTransition} from './types'; import {useDerivedTransform} from './utilities'; +import {ActiveContext} from '../components/SortableContext'; +const NullContext = createContext(null); export interface Arguments extends Omit, Pick { animateLayoutChanges?: AnimateLayoutChanges; disabled?: boolean | Disabled; - getNewIndex?: NewIndexGetter; strategy?: SortingStrategy; transition?: SortableTransition | null; } @@ -41,7 +37,6 @@ export function useSortable({ attributes: userDefinedAttributes, disabled: localDisabled, data: customData, - getNewIndex = defaultNewIndexGetter, id, strategy: localStrategy, resizeObserverConfig, @@ -50,13 +45,12 @@ export function useSortable({ const { items, containerId, - activeIndex, disabled: globalDisabled, disableTransforms, - sortedRects, - overIndex, useDragOverlay, strategy: globalStrategy, + useMyNewIndex, + previousSortingStateRef, } = useContext(Context); const disabled: Disabled = normalizeLocalDisabled( localDisabled, @@ -105,52 +99,55 @@ export function useSortable({ }, disabled: disabled.draggable, }); + + const activeSortable = useContext(isDragging ? ActiveContext : NullContext); + let shouldDisplaceDragSource = false; + let finalTransform = null; + if (isDragging) { + const activeIndex = activeSortable?.index; + const sortedRects = activeSortable?.sortedRects; + const overIndex = activeSortable?.overIndex; + + const displaceItem = + !disableTransforms && + isValidIndex(activeIndex) && + isValidIndex(overIndex); + + const shouldDisplaceDragSource = !useDragOverlay; + const dragSourceDisplacement = + shouldDisplaceDragSource && displaceItem ? transform : null; + + const strategy = localStrategy ?? globalStrategy; + finalTransform = displaceItem + ? dragSourceDisplacement ?? + strategy({ + rects: sortedRects, + activeNodeRect, + activeIndex, + overIndex, + index, + }) + : null; + } + const setNodeRef = useCombinedRefs(setDroppableNodeRef, setDraggableNodeRef); - const isSorting = Boolean(active); - const displaceItem = - isSorting && - !disableTransforms && - isValidIndex(activeIndex) && - isValidIndex(overIndex); - const shouldDisplaceDragSource = !useDragOverlay && isDragging; - const dragSourceDisplacement = - shouldDisplaceDragSource && displaceItem ? transform : null; - const strategy = localStrategy ?? globalStrategy; - const finalTransform = displaceItem - ? dragSourceDisplacement ?? - strategy({ - rects: sortedRects, - activeNodeRect, - activeIndex, - overIndex, - index, - }) - : null; - const newIndex = - isValidIndex(activeIndex) && isValidIndex(overIndex) - ? getNewIndex({id, items, activeIndex, overIndex}) - : index; - const activeId = active?.id; - const previous = useRef({ - activeId, - items, - newIndex, - containerId, - }); - const itemsHaveChanged = items !== previous.current.items; + + const newIndex = useMyNewIndex(id, index); + const prevNewIndex = useRef(newIndex); + const itemsHaveChanged = items !== previousSortingStateRef.current.items; const shouldAnimateLayoutChanges = animateLayoutChanges({ active, containerId, isDragging, - isSorting, + isSorting: isDragging, id, index, items, - newIndex: previous.current.newIndex, - previousItems: previous.current.items, - previousContainerId: previous.current.containerId, + newIndex: prevNewIndex.current, + previousItems: previousSortingStateRef.current.items, + previousContainerId: previousSortingStateRef.current.containerId, transition, - wasDragging: previous.current.activeId != null, + wasDragging: previousSortingStateRef.current.activeId != null, }); const derivedTransform = useDerivedTransform({ @@ -161,39 +158,13 @@ export function useSortable({ }); useEffect(() => { - if (isSorting && previous.current.newIndex !== newIndex) { - previous.current.newIndex = newIndex; - } - - if (containerId !== previous.current.containerId) { - previous.current.containerId = containerId; + if (prevNewIndex.current !== newIndex) { + prevNewIndex.current = newIndex; } - - if (items !== previous.current.items) { - previous.current.items = items; - } - }, [isSorting, newIndex, containerId, items]); - - useEffect(() => { - if (activeId === previous.current.activeId) { - return; - } - - if (activeId && !previous.current.activeId) { - previous.current.activeId = activeId; - return; - } - - const timeoutId = setTimeout(() => { - previous.current.activeId = activeId; - }, 50); - - return () => clearTimeout(timeoutId); - }, [activeId]); + }, [newIndex]); return { active, - activeIndex, attributes, data, rect, @@ -201,11 +172,9 @@ export function useSortable({ newIndex, items, isOver, - isSorting, isDragging, listeners, node, - overIndex, over, setNodeRef, setActivatorNodeRef, @@ -220,7 +189,7 @@ export function useSortable({ // Temporarily disable transitions for a single frame to set up derived transforms derivedTransform || // Or to prevent items jumping to back to their "new" position when items change - (itemsHaveChanged && previous.current.newIndex === index) + (itemsHaveChanged && prevNewIndex.current === index) ) { return disabledTransition; } @@ -232,7 +201,7 @@ export function useSortable({ return undefined; } - if (isSorting || shouldAnimateLayoutChanges) { + if (shouldAnimateLayoutChanges) { return CSS.Transition.toString({ ...transition, property: transitionProperty, diff --git a/packages/sortable/src/index.ts b/packages/sortable/src/index.ts index 58ebbd15..6dbb8978 100644 --- a/packages/sortable/src/index.ts +++ b/packages/sortable/src/index.ts @@ -5,11 +5,7 @@ export { defaultAnimateLayoutChanges, defaultNewIndexGetter, } from './hooks'; -export type { - UseSortableArguments, - AnimateLayoutChanges, - NewIndexGetter, -} from './hooks'; +export type {UseSortableArguments, AnimateLayoutChanges} from './hooks'; export { horizontalListSortingStrategy, rectSortingStrategy, @@ -19,4 +15,4 @@ export { export {sortableKeyboardCoordinates} from './sensors'; export {arrayMove, arraySwap} from './utilities'; export {hasSortableData} from './types'; -export type {SortableData, SortingStrategy} from './types'; +export type {SortableData, SortingStrategy, NewIndexGetter} from './types'; diff --git a/packages/sortable/src/types/index.ts b/packages/sortable/src/types/index.ts index 17412e64..8880abe5 100644 --- a/packages/sortable/src/types/index.ts +++ b/packages/sortable/src/types/index.ts @@ -2,3 +2,4 @@ export type {Disabled} from './disabled'; export type {SortableData} from './data'; export type {SortingStrategy} from './strategies'; export {hasSortableData} from './type-guard'; +export type {NewIndexGetter, NewIndexGetterArguments} from './indexGetter'; diff --git a/packages/sortable/src/types/indexGetter.ts b/packages/sortable/src/types/indexGetter.ts new file mode 100644 index 00000000..b3beec82 --- /dev/null +++ b/packages/sortable/src/types/indexGetter.ts @@ -0,0 +1,10 @@ +import type {UniqueIdentifier} from '@dnd-kit/core'; + +export interface NewIndexGetterArguments { + id: UniqueIdentifier; + items: UniqueIdentifier[]; + activeIndex: number; + overIndex: number; +} + +export type NewIndexGetter = (args: NewIndexGetterArguments) => number; From a873505365c6281352fa98ea395c477075c0d12e Mon Sep 17 00:00:00 2001 From: Alissa Date: Sat, 1 Apr 2023 06:49:18 +0300 Subject: [PATCH 12/32] some fixes --- .../src/components/SortableContext.tsx | 8 +++---- .../sortable/src/components/sortingAPI.tsx | 22 ++++++++++++++----- stories/2 - Presets/Sortable/Sortable.tsx | 18 +++++++-------- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/packages/sortable/src/components/SortableContext.tsx b/packages/sortable/src/components/SortableContext.tsx index da551505..7900d878 100644 --- a/packages/sortable/src/components/SortableContext.tsx +++ b/packages/sortable/src/components/SortableContext.tsx @@ -78,14 +78,14 @@ export function SortableContext({ [userDefinedItems] ); const sortingAPI = useMemo( - () => createSortingAPI(items, activeAndOverAPI, getNewIndex), - [activeAndOverAPI, items, getNewIndex] + () => createSortingAPI(activeAndOverAPI, getNewIndex), + [] ); useEffect(() => { return sortingAPI.clear; - }); + }, []); const isDragging = active != null; - const activeIndex = sortingAPI.getActiveIndex(); + const activeIndex = active ? items.indexOf(active.id) : -1; const overIndex = sortingAPI.getOverIndex(); const previousItemsRef = useRef(items); const itemsHaveChanged = !itemsEqual(items, previousItemsRef.current); diff --git a/packages/sortable/src/components/sortingAPI.tsx b/packages/sortable/src/components/sortingAPI.tsx index 413e2ac8..efd25690 100644 --- a/packages/sortable/src/components/sortingAPI.tsx +++ b/packages/sortable/src/components/sortingAPI.tsx @@ -1,18 +1,18 @@ import type {UniqueIdentifier, DndContextDescriptor} from '@dnd-kit/core'; -import {useSyncExternalStore} from 'use-sync-external-store'; +import {useSyncExternalStore} from 'use-sync-external-store/shim'; import {defaultNewIndexGetter} from '../hooks/defaults'; import {isValidIndex} from '../utilities'; export function createSortingAPI( - items: UniqueIdentifier[], activeAndOverAPI: DndContextDescriptor['activeAndOverAPI'], getNewIndex = defaultNewIndexGetter ) { let activeIndex: number = -1; let overIndex: number = -1; + let items: UniqueIdentifier[] = []; calculateIndexes(); - const registry: (() => void)[] = []; + let registry: (() => void)[] = []; function subscribe(listener: () => void) { registry.push(listener); @@ -23,17 +23,28 @@ export function createSortingAPI( function calculateIndexes() { const active = activeAndOverAPI.getActive(); - activeIndex = active ? items.indexOf(active.id) : -1; + if (!active) { + overIndex = -1; + activeIndex = -1; + return; + } const over = activeAndOverAPI.getOver(); + items = active?.data.current?.sortable.items; + + activeIndex = active ? items.indexOf(active.id) : -1; overIndex = over ? items.indexOf(over.id) : -1; } const unsubscribeFromActiveAndOver = activeAndOverAPI.subscribe(() => { + calculateIndexes(); registry.forEach((li) => li()); }); return { - clear: unsubscribeFromActiveAndOver, + clear: () => { + unsubscribeFromActiveAndOver(); + registry = []; + }, useMyNewIndex: (id: UniqueIdentifier, currentIndex: number) => { return useSyncExternalStore(subscribe, () => { return isValidIndex(activeIndex) && isValidIndex(overIndex) @@ -41,7 +52,6 @@ export function createSortingAPI( : currentIndex; }); }, - getActiveIndex: () => activeIndex, getOverIndex: () => overIndex, }; } diff --git a/stories/2 - Presets/Sortable/Sortable.tsx b/stories/2 - Presets/Sortable/Sortable.tsx index 6272c5ae..2d492aa7 100644 --- a/stories/2 - Presets/Sortable/Sortable.tsx +++ b/stories/2 - Presets/Sortable/Sortable.tsx @@ -206,7 +206,11 @@ export function Sortable({ modifiers={modifiers} > - + {items.map((value, index) => ( ))} @@ -291,7 +294,6 @@ function SortableDragOverlay({ interface SortableItemProps { animateLayoutChanges?: AnimateLayoutChanges; disabled?: boolean; - getNewIndex?: NewIndexGetter; id: UniqueIdentifier; index: number; handle: boolean; @@ -305,7 +307,6 @@ interface SortableItemProps { export function SortableItem({ disabled, animateLayoutChanges, - getNewIndex, handle, id, index, @@ -319,9 +320,7 @@ export function SortableItem({ active, attributes, isDragging, - isSorting, listeners, - overIndex, setNodeRef, setActivatorNodeRef, transform, @@ -330,7 +329,6 @@ export function SortableItem({ id, animateLayoutChanges, disabled, - getNewIndex, }); return ( @@ -339,7 +337,7 @@ export function SortableItem({ value={id} disabled={disabled} dragging={isDragging} - sorting={isSorting} + sorting={true} handle={handle} handleProps={ handle @@ -354,8 +352,8 @@ export function SortableItem({ index, id, isDragging, - isSorting, - overIndex, + isSorting: true, + overIndex: 3, })} onRemove={onRemove ? () => onRemove(id) : undefined} transform={transform} From cd2d0d4e375dd0f0a2c51f758cc347ddee17ad25 Mon Sep 17 00:00:00 2001 From: Alissa Date: Sat, 1 Apr 2023 07:00:57 +0300 Subject: [PATCH 13/32] calculate hasActive in useSortable --- packages/sortable/src/hooks/useSortable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sortable/src/hooks/useSortable.ts b/packages/sortable/src/hooks/useSortable.ts index 6560b8a6..bc5c1cb5 100644 --- a/packages/sortable/src/hooks/useSortable.ts +++ b/packages/sortable/src/hooks/useSortable.ts @@ -106,7 +106,7 @@ export function useSortable({ disabled: disabled.draggable, }); const setNodeRef = useCombinedRefs(setDroppableNodeRef, setDraggableNodeRef); - const isSorting = Boolean(active); + const isSorting = activeIndex >= 0; const displaceItem = isSorting && !disableTransforms && From b356522baf78d7f497b44d3a5efbbb9dd0a019c2 Mon Sep 17 00:00:00 2001 From: Alissa Date: Sat, 1 Apr 2023 07:42:08 +0300 Subject: [PATCH 14/32] fix default context for drag overlay --- .../src/components/DndContext/DndContext.tsx | 1 + packages/core/src/hooks/useDraggable.ts | 2 ++ packages/core/src/store/context.ts | 32 +++++-------------- packages/core/src/store/types.ts | 3 ++ packages/sortable/src/hooks/useSortable.ts | 3 +- 5 files changed, 16 insertions(+), 25 deletions(-) diff --git a/packages/core/src/components/DndContext/DndContext.tsx b/packages/core/src/components/DndContext/DndContext.tsx index 6856cfd2..278ae172 100644 --- a/packages/core/src/components/DndContext/DndContext.tsx +++ b/packages/core/src/components/DndContext/DndContext.tsx @@ -681,6 +681,7 @@ export const DndContext = memo(function DndContext({ draggableNodes, over, measureDroppableContainers, + isDefaultContext: false, }; return context; diff --git a/packages/core/src/hooks/useDraggable.ts b/packages/core/src/hooks/useDraggable.ts index ab887ebe..840860f7 100644 --- a/packages/core/src/hooks/useDraggable.ts +++ b/packages/core/src/hooks/useDraggable.ts @@ -55,6 +55,7 @@ export function useDraggable({ ariaDescribedById, draggableNodes, over, + isDefaultContext, } = useContext(InternalContext); const { role = defaultRole, @@ -121,5 +122,6 @@ export function useDraggable({ setNodeRef, setActivatorNodeRef, transform, + isDefaultContext, }; } diff --git a/packages/core/src/store/context.ts b/packages/core/src/store/context.ts index 50b1bb48..5eaabb1b 100644 --- a/packages/core/src/store/context.ts +++ b/packages/core/src/store/context.ts @@ -3,12 +3,7 @@ import {createContext} from 'react'; import {noop} from '../utilities/other'; import {defaultMeasuringConfiguration} from '../components/DndContext/defaults'; import {DroppableContainersMap} from './constructors'; -import type { - Active, - InternalContextDescriptor, - PublicContextDescriptor, -} from './types'; -import type {ClientRect} from '../types'; +import type {InternalContextDescriptor, PublicContextDescriptor} from './types'; export const defaultPublicContext: PublicContextDescriptor = { activatorEvent: null, @@ -45,24 +40,13 @@ export const defaultInternalContext: InternalContextDescriptor = { draggableNodes: new Map(), over: null, measureDroppableContainers: noop, - useMyActive: function (): Active | null { - throw new Error('Function not implemented.'); - }, - useGloablActive: function (): Active | null { - throw new Error('Function not implemented.'); - }, - useHasActive: function (): boolean { - throw new Error('Function not implemented.'); - }, - useMyActivatorEvent: function (): Event | null { - throw new Error('Function not implemented.'); - }, - useGlobalActivatorEvent: function (): Event | null { - throw new Error('Function not implemented.'); - }, - useMyActiveNodeRect: function (): ClientRect | null { - throw new Error('Function not implemented.'); - }, + useMyActive: () => null, + useGloablActive: () => null, + useHasActive: () => false, + useMyActivatorEvent: () => null, + useGlobalActivatorEvent: () => null, + useMyActiveNodeRect: () => null, + isDefaultContext: true, }; export const InternalContext = createContext( diff --git a/packages/core/src/store/types.ts b/packages/core/src/store/types.ts index 5d8d44a6..667cf023 100644 --- a/packages/core/src/store/types.ts +++ b/packages/core/src/store/types.ts @@ -111,4 +111,7 @@ export interface InternalContextDescriptor { draggableNodes: DraggableNodes; over: Over | null; measureDroppableContainers(ids: UniqueIdentifier[]): void; + //this is a temparary solution, since we don't return general active element from useDraggable hook + //I added this to know if a sortable item is inside an overlay + isDefaultContext: boolean; } diff --git a/packages/sortable/src/hooks/useSortable.ts b/packages/sortable/src/hooks/useSortable.ts index bc5c1cb5..b61d54c7 100644 --- a/packages/sortable/src/hooks/useSortable.ts +++ b/packages/sortable/src/hooks/useSortable.ts @@ -96,6 +96,7 @@ export function useSortable({ over, setActivatorNodeRef, transform, + isDefaultContext, } = useDraggable({ id, data, @@ -106,7 +107,7 @@ export function useSortable({ disabled: disabled.draggable, }); const setNodeRef = useCombinedRefs(setDroppableNodeRef, setDraggableNodeRef); - const isSorting = activeIndex >= 0; + const isSorting = activeIndex >= 0 && !isDefaultContext; const displaceItem = isSorting && !disableTransforms && From 1affa4f87eedb95fc13217e7983400229086eb1d Mon Sep 17 00:00:00 2001 From: Alissa Date: Sat, 1 Apr 2023 19:10:17 +0300 Subject: [PATCH 15/32] can drag over in sortable without re-render all. some small issues to fix still --- packages/core/src/store/context.ts | 4 + .../src/components/SortableContext.tsx | 22 +++-- .../sortable/src/components/sortingAPI.tsx | 84 ++++++++++++++++--- packages/sortable/src/hooks/useSortable.ts | 66 +++++---------- packages/sortable/src/types/strategies.ts | 2 + 5 files changed, 116 insertions(+), 62 deletions(-) diff --git a/packages/core/src/store/context.ts b/packages/core/src/store/context.ts index 838c2ec7..83ae683e 100644 --- a/packages/core/src/store/context.ts +++ b/packages/core/src/store/context.ts @@ -4,6 +4,7 @@ import {noop} from '../utilities/other'; import {defaultMeasuringConfiguration} from '../components/DndContext/defaults'; import {DroppableContainersMap} from './constructors'; import type {InternalContextDescriptor, PublicContextDescriptor} from './types'; +import {createActiveAndOverAPI} from '../components/DndContext/activeAndOverAPI'; export const defaultPublicContext: PublicContextDescriptor = { activatorEvent: null, @@ -29,6 +30,9 @@ export const defaultPublicContext: PublicContextDescriptor = { measureDroppableContainers: noop, windowRect: null, measuringScheduled: false, + activeAndOverAPI: createActiveAndOverAPI({ + current: {initial: null, translated: null}, + }), }; export const defaultInternalContext: InternalContextDescriptor = { diff --git a/packages/sortable/src/components/SortableContext.tsx b/packages/sortable/src/components/SortableContext.tsx index 7900d878..aceda80d 100644 --- a/packages/sortable/src/components/SortableContext.tsx +++ b/packages/sortable/src/components/SortableContext.tsx @@ -3,7 +3,7 @@ import {useDndContext, ClientRect, UniqueIdentifier} from '@dnd-kit/core'; import {useIsomorphicLayoutEffect, useUniqueId} from '@dnd-kit/utilities'; import type {Disabled, NewIndexGetter, SortingStrategy} from '../types'; -import {getSortedRects, itemsEqual, normalizeDisabled} from '../utilities'; +import {getSortedRects, normalizeDisabled} from '../utilities'; import {rectSortingStrategy} from '../strategies'; import {createSortingAPI} from './sortingAPI'; import {usePreviousSortingStateRef} from './usePreviousSortingState'; @@ -29,6 +29,11 @@ interface ContextDescriptor { strategy: SortingStrategy; useMyNewIndex: (id: UniqueIdentifier, currentIndex: number) => number; previousSortingStateRef: ReturnType; + useMyStrategyValue: ( + id: UniqueIdentifier, + currentIndex: number, + activeNodeRect: ClientRect | null + ) => string | null; } export const Context = React.createContext({ @@ -45,6 +50,7 @@ export const Context = React.createContext({ previousSortingStateRef: { current: {activeId: null, containerId: '', items: []}, }, + useMyStrategyValue: () => null, }); export const ActiveContext = React.createContext({ @@ -78,19 +84,22 @@ export function SortableContext({ [userDefinedItems] ); const sortingAPI = useMemo( - () => createSortingAPI(activeAndOverAPI, getNewIndex), + () => createSortingAPI(activeAndOverAPI, getNewIndex, strategy), + // eslint-disable-next-line react-hooks/exhaustive-deps [] ); useEffect(() => { + sortingAPI.init(); return sortingAPI.clear; - }, []); + }, [sortingAPI]); + + sortingAPI.setSortingInfo(droppableRects, items); const isDragging = active != null; const activeIndex = active ? items.indexOf(active.id) : -1; const overIndex = sortingAPI.getOverIndex(); const previousItemsRef = useRef(items); - const itemsHaveChanged = !itemsEqual(items, previousItemsRef.current); - const disableTransforms = - (overIndex !== -1 && activeIndex === -1) || itemsHaveChanged; + const itemsHaveChanged = sortingAPI.getItemsHaveChanged(); + const disableTransforms = !sortingAPI.getShouldDisplaceItems(); const disabled = normalizeDisabled(disabledProp); useIsomorphicLayoutEffect(() => { @@ -126,6 +135,7 @@ export function SortableContext({ strategy, useMyNewIndex: sortingAPI.useMyNewIndex, previousSortingStateRef, + useMyStrategyValue: sortingAPI.useMyStrategyValue, }), // eslint-disable-next-line react-hooks/exhaustive-deps [ diff --git a/packages/sortable/src/components/sortingAPI.tsx b/packages/sortable/src/components/sortingAPI.tsx index efd25690..eb7441d2 100644 --- a/packages/sortable/src/components/sortingAPI.tsx +++ b/packages/sortable/src/components/sortingAPI.tsx @@ -1,15 +1,25 @@ -import type {UniqueIdentifier, DndContextDescriptor} from '@dnd-kit/core'; +import type { + UniqueIdentifier, + DndContextDescriptor, + ClientRect, +} from '@dnd-kit/core'; import {useSyncExternalStore} from 'use-sync-external-store/shim'; import {defaultNewIndexGetter} from '../hooks/defaults'; -import {isValidIndex} from '../utilities'; +import type {SortingStrategy} from '../types'; +import {getSortedRects, isValidIndex, itemsEqual} from '../utilities'; export function createSortingAPI( activeAndOverAPI: DndContextDescriptor['activeAndOverAPI'], - getNewIndex = defaultNewIndexGetter + getNewIndex = defaultNewIndexGetter, + strategy: SortingStrategy ) { + let unsubscribeFromActiveAndOver = () => {}; let activeIndex: number = -1; let overIndex: number = -1; let items: UniqueIdentifier[] = []; + let itemsHaveChanged = false; + let droppableRects: Map = new Map(); + let sortedRecs: ClientRect[] = []; calculateIndexes(); let registry: (() => void)[] = []; @@ -29,21 +39,47 @@ export function createSortingAPI( return; } const over = activeAndOverAPI.getOver(); - items = active?.data.current?.sortable.items; - activeIndex = active ? items.indexOf(active.id) : -1; overIndex = over ? items.indexOf(over.id) : -1; } - const unsubscribeFromActiveAndOver = activeAndOverAPI.subscribe(() => { - calculateIndexes(); - registry.forEach((li) => li()); - }); + function shouldDisplaceItems() { + return ( + isValidIndex(overIndex) && isValidIndex(activeIndex) && !itemsHaveChanged + ); + } return { + setSortingInfo: ( + droppable: Map, + newItems: UniqueIdentifier[] + ) => { + // let changed = false; + if (droppableRects !== droppable) { + droppableRects = droppable; + sortedRecs = getSortedRects(newItems, droppableRects); + // changed = true; + } + if (newItems !== items && !itemsEqual(newItems, items)) { + items = newItems; + // calculateIndexes(); + itemsHaveChanged = true; + // changed = true; + } else { + itemsHaveChanged = false; + } + // if (changed) { + // registry.forEach((li) => li()); + // } + }, + init: () => { + unsubscribeFromActiveAndOver = activeAndOverAPI.subscribe(() => { + calculateIndexes(); + registry.forEach((li) => li()); + }); + }, clear: () => { unsubscribeFromActiveAndOver(); - registry = []; }, useMyNewIndex: (id: UniqueIdentifier, currentIndex: number) => { return useSyncExternalStore(subscribe, () => { @@ -52,6 +88,34 @@ export function createSortingAPI( : currentIndex; }); }, + + useMyStrategyValue( + id: UniqueIdentifier, + currentIndex: number, + activeNodeRect: ClientRect | null + ) { + return useSyncExternalStore(subscribe, () => { + if (!shouldDisplaceItems() || currentIndex === activeIndex) { + return null; + } + const delta = strategy({ + id, + activeNodeRect, + rects: sortedRecs, + activeIndex, + overIndex, + index: currentIndex, + }); + + const deltaJson = JSON.stringify(delta); + if (deltaJson === JSON.stringify({x: 0, y: 0, scaleX: 1, scaleY: 1})) { + return null; + } + return deltaJson; + }); + }, getOverIndex: () => overIndex, + getItemsHaveChanged: () => itemsHaveChanged, + getShouldDisplaceItems: () => shouldDisplaceItems(), }; } diff --git a/packages/sortable/src/hooks/useSortable.ts b/packages/sortable/src/hooks/useSortable.ts index 074a4f21..d34409fb 100644 --- a/packages/sortable/src/hooks/useSortable.ts +++ b/packages/sortable/src/hooks/useSortable.ts @@ -1,4 +1,4 @@ -import {createContext, useContext, useEffect, useMemo, useRef} from 'react'; +import {useContext, useEffect, useMemo, useRef} from 'react'; import { useDraggable, useDroppable, @@ -10,7 +10,6 @@ import {CSS, isKeyboardEvent, useCombinedRefs} from '@dnd-kit/utilities'; import {Context} from '../components'; import type {Disabled, SortableData, SortingStrategy} from '../types'; -import {isValidIndex} from '../utilities'; import { defaultAnimateLayoutChanges, defaultAttributes, @@ -20,8 +19,6 @@ import { } from './defaults'; import type {AnimateLayoutChanges, SortableTransition} from './types'; import {useDerivedTransform} from './utilities'; -import {ActiveContext} from '../components/SortableContext'; -const NullContext = createContext(null); export interface Arguments extends Omit, @@ -38,7 +35,8 @@ export function useSortable({ disabled: localDisabled, data: customData, id, - strategy: localStrategy, + //TODO: deal with local strategy.... + // strategy: localStrategy, resizeObserverConfig, transition = defaultTransition, }: Arguments) { @@ -48,14 +46,15 @@ export function useSortable({ disabled: globalDisabled, disableTransforms, useDragOverlay, - strategy: globalStrategy, useMyNewIndex, previousSortingStateRef, + useMyStrategyValue, } = useContext(Context); const disabled: Disabled = normalizeLocalDisabled( localDisabled, globalDisabled ); + const index = items.indexOf(id); const data = useMemo( () => ({sortable: {containerId, index, items}, ...customData}), @@ -100,38 +99,16 @@ export function useSortable({ disabled: disabled.draggable, }); - const activeSortable = useContext(isDragging ? ActiveContext : NullContext); - let shouldDisplaceDragSource = false; - let finalTransform = null; - if (isDragging) { - const activeIndex = activeSortable?.index; - const sortedRects = activeSortable?.sortedRects; - const overIndex = activeSortable?.overIndex; - - const displaceItem = - !disableTransforms && - isValidIndex(activeIndex) && - isValidIndex(overIndex); - - const shouldDisplaceDragSource = !useDragOverlay; - const dragSourceDisplacement = - shouldDisplaceDragSource && displaceItem ? transform : null; - - const strategy = localStrategy ?? globalStrategy; - finalTransform = displaceItem - ? dragSourceDisplacement ?? - strategy({ - rects: sortedRects, - activeNodeRect, - activeIndex, - overIndex, - index, - }) - : null; - } - const setNodeRef = useCombinedRefs(setDroppableNodeRef, setDraggableNodeRef); + const shouldDisplaceDragSource = + !disableTransforms && !useDragOverlay && isDragging; + const dragSourceDisplacement = shouldDisplaceDragSource ? transform : null; + const otherItemDisplacement = useMyStrategyValue(id, index, activeNodeRect); + const finalTransform = + dragSourceDisplacement ?? + (otherItemDisplacement ? JSON.parse(otherItemDisplacement) : null); + const newIndex = useMyNewIndex(id, index); const prevNewIndex = useRef(newIndex); const itemsHaveChanged = items !== previousSortingStateRef.current.items; @@ -139,7 +116,8 @@ export function useSortable({ active, containerId, isDragging, - isSorting: isDragging, + //this value was true as long as there is an active item... not sure what was the purpose + isSorting: true, id, index, items, @@ -152,7 +130,7 @@ export function useSortable({ const derivedTransform = useDerivedTransform({ disabled: !shouldAnimateLayoutChanges, - index, + index: newIndex, node, rect, }); @@ -201,14 +179,10 @@ export function useSortable({ return undefined; } - if (shouldAnimateLayoutChanges) { - return CSS.Transition.toString({ - ...transition, - property: transitionProperty, - }); - } - - return undefined; + return CSS.Transition.toString({ + ...transition, + property: transitionProperty, + }); } } diff --git a/packages/sortable/src/types/strategies.ts b/packages/sortable/src/types/strategies.ts index 5ea719df..300d548d 100644 --- a/packages/sortable/src/types/strategies.ts +++ b/packages/sortable/src/types/strategies.ts @@ -1,7 +1,9 @@ import type {ClientRect} from '@dnd-kit/core'; import type {Transform} from '@dnd-kit/utilities'; +import type {UniqueIdentifier} from 'packages/core/dist'; export type SortingStrategy = (args: { + id: UniqueIdentifier; activeNodeRect: ClientRect | null; activeIndex: number; index: number; From 95199cbab6036dadea62d50f2bb476fd28d4a8be Mon Sep 17 00:00:00 2001 From: Alissa Date: Sat, 1 Apr 2023 19:41:29 +0300 Subject: [PATCH 16/32] clean up --- .../src/components/SortableContext.tsx | 34 +++---------------- .../sortable/src/components/sortingAPI.tsx | 9 +---- 2 files changed, 6 insertions(+), 37 deletions(-) diff --git a/packages/sortable/src/components/SortableContext.tsx b/packages/sortable/src/components/SortableContext.tsx index aceda80d..d34bb9c5 100644 --- a/packages/sortable/src/components/SortableContext.tsx +++ b/packages/sortable/src/components/SortableContext.tsx @@ -3,7 +3,7 @@ import {useDndContext, ClientRect, UniqueIdentifier} from '@dnd-kit/core'; import {useIsomorphicLayoutEffect, useUniqueId} from '@dnd-kit/utilities'; import type {Disabled, NewIndexGetter, SortingStrategy} from '../types'; -import {getSortedRects, normalizeDisabled} from '../utilities'; +import {normalizeDisabled} from '../utilities'; import {rectSortingStrategy} from '../strategies'; import {createSortingAPI} from './sortingAPI'; import {usePreviousSortingStateRef} from './usePreviousSortingState'; @@ -26,7 +26,6 @@ interface ContextDescriptor { disableTransforms: boolean; items: UniqueIdentifier[]; useDragOverlay: boolean; - strategy: SortingStrategy; useMyNewIndex: (id: UniqueIdentifier, currentIndex: number) => number; previousSortingStateRef: ReturnType; useMyStrategyValue: ( @@ -41,7 +40,6 @@ export const Context = React.createContext({ disableTransforms: false, items: [], useDragOverlay: false, - strategy: rectSortingStrategy, disabled: { draggable: false, droppable: false, @@ -53,12 +51,6 @@ export const Context = React.createContext({ useMyStrategyValue: () => null, }); -export const ActiveContext = React.createContext({ - activeIndex: -1, - overIndex: -1, - sortedRects: [] as ClientRect[], -}); - export function SortableContext({ children, id, @@ -93,10 +85,8 @@ export function SortableContext({ return sortingAPI.clear; }, [sortingAPI]); - sortingAPI.setSortingInfo(droppableRects, items); + sortingAPI.silentSetSortingInfo(droppableRects, items); const isDragging = active != null; - const activeIndex = active ? items.indexOf(active.id) : -1; - const overIndex = sortingAPI.getOverIndex(); const previousItemsRef = useRef(items); const itemsHaveChanged = sortingAPI.getItemsHaveChanged(); const disableTransforms = !sortingAPI.getShouldDisplaceItems(); @@ -112,14 +102,6 @@ export function SortableContext({ previousItemsRef.current = items; }, [items]); - const activeContextValue = useMemo( - () => ({ - activeIndex, - overIndex, - sortedRects: getSortedRects(items, droppableRects), - }), - [activeIndex, droppableRects, items, overIndex] - ); const previousSortingStateRef = usePreviousSortingStateRef({ activeId: active?.id || null, containerId, @@ -132,7 +114,6 @@ export function SortableContext({ disableTransforms, items, useDragOverlay, - strategy, useMyNewIndex: sortingAPI.useMyNewIndex, previousSortingStateRef, useMyStrategyValue: sortingAPI.useMyStrategyValue, @@ -145,15 +126,10 @@ export function SortableContext({ disableTransforms, items, useDragOverlay, - strategy, + sortingAPI, + previousSortingStateRef, ] ); - return ( - - - {children} - - - ); + return {children}; } diff --git a/packages/sortable/src/components/sortingAPI.tsx b/packages/sortable/src/components/sortingAPI.tsx index eb7441d2..173614c2 100644 --- a/packages/sortable/src/components/sortingAPI.tsx +++ b/packages/sortable/src/components/sortingAPI.tsx @@ -50,27 +50,20 @@ export function createSortingAPI( } return { - setSortingInfo: ( + silentSetSortingInfo: ( droppable: Map, newItems: UniqueIdentifier[] ) => { - // let changed = false; if (droppableRects !== droppable) { droppableRects = droppable; sortedRecs = getSortedRects(newItems, droppableRects); - // changed = true; } if (newItems !== items && !itemsEqual(newItems, items)) { items = newItems; - // calculateIndexes(); itemsHaveChanged = true; - // changed = true; } else { itemsHaveChanged = false; } - // if (changed) { - // registry.forEach((li) => li()); - // } }, init: () => { unsubscribeFromActiveAndOver = activeAndOverAPI.subscribe(() => { From 503adb84814620010cc443db98af036645153486 Mon Sep 17 00:00:00 2001 From: Alissa Date: Sun, 2 Apr 2023 09:47:53 +0300 Subject: [PATCH 17/32] return strategy delta for active as well --- packages/sortable/src/components/sortingAPI.tsx | 8 ++------ stories/3 - Examples/Tree/SortableTree.tsx | 7 ++++--- .../Tree/components/TreeItem/SortableTreeItem.tsx | 9 +++++---- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/sortable/src/components/sortingAPI.tsx b/packages/sortable/src/components/sortingAPI.tsx index 173614c2..60c9912d 100644 --- a/packages/sortable/src/components/sortingAPI.tsx +++ b/packages/sortable/src/components/sortingAPI.tsx @@ -88,7 +88,7 @@ export function createSortingAPI( activeNodeRect: ClientRect | null ) { return useSyncExternalStore(subscribe, () => { - if (!shouldDisplaceItems() || currentIndex === activeIndex) { + if (!shouldDisplaceItems()) { return null; } const delta = strategy({ @@ -100,11 +100,7 @@ export function createSortingAPI( index: currentIndex, }); - const deltaJson = JSON.stringify(delta); - if (deltaJson === JSON.stringify({x: 0, y: 0, scaleX: 1, scaleY: 1})) { - return null; - } - return deltaJson; + return JSON.stringify(delta); }); }, getOverIndex: () => overIndex, diff --git a/stories/3 - Examples/Tree/SortableTree.tsx b/stories/3 - Examples/Tree/SortableTree.tsx index 32d4bd12..244090e9 100644 --- a/stories/3 - Examples/Tree/SortableTree.tsx +++ b/stories/3 - Examples/Tree/SortableTree.tsx @@ -156,9 +156,10 @@ export function SortableTree({ }) ); - const sortedIds = useMemo(() => flattenedItems.map(({id}) => id), [ - flattenedItems, - ]); + const sortedIds = useMemo( + () => flattenedItems.map(({id}) => id), + [flattenedItems] + ); const activeItem = activeId ? flattenedItems.find(({id}) => id === activeId) : null; diff --git a/stories/3 - Examples/Tree/components/TreeItem/SortableTreeItem.tsx b/stories/3 - Examples/Tree/components/TreeItem/SortableTreeItem.tsx index dedfc69a..149d850a 100644 --- a/stories/3 - Examples/Tree/components/TreeItem/SortableTreeItem.tsx +++ b/stories/3 - Examples/Tree/components/TreeItem/SortableTreeItem.tsx @@ -10,14 +10,15 @@ interface Props extends TreeItemProps { id: UniqueIdentifier; } -const animateLayoutChanges: AnimateLayoutChanges = ({isSorting, wasDragging}) => - isSorting || wasDragging ? false : true; +const animateLayoutChanges: AnimateLayoutChanges = ({ + wasDragging, + isDragging, +}) => (isDragging || wasDragging ? false : true); export function SortableTreeItem({id, depth, ...props}: Props) { const { attributes, isDragging, - isSorting, listeners, setDraggableNodeRef, setDroppableNodeRef, @@ -40,7 +41,7 @@ export function SortableTreeItem({id, depth, ...props}: Props) { depth={depth} ghost={isDragging} disableSelection={iOS} - disableInteraction={isSorting} + disableInteraction={isDragging} handleProps={{ ...attributes, ...listeners, From 1ab04f7599bcf1d79942a7a5a5f74357dfcd2666 Mon Sep 17 00:00:00 2001 From: Alissa Date: Sun, 2 Apr 2023 19:39:18 +0300 Subject: [PATCH 18/32] do not animate drop by default --- packages/sortable/src/hooks/useSortable.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/sortable/src/hooks/useSortable.ts b/packages/sortable/src/hooks/useSortable.ts index d34409fb..06fb4dfb 100644 --- a/packages/sortable/src/hooks/useSortable.ts +++ b/packages/sortable/src/hooks/useSortable.ts @@ -179,10 +179,15 @@ export function useSortable({ return undefined; } - return CSS.Transition.toString({ - ...transition, - property: transitionProperty, - }); + const isDropping = + previousSortingStateRef.current.activeId === id && !isDragging; + if (shouldAnimateLayoutChanges || !isDropping) { + return CSS.Transition.toString({ + ...transition, + property: transitionProperty, + }); + } + return undefined; } } From 6dbc5f85f99b613038ad6bd39906c6c4aade4683 Mon Sep 17 00:00:00 2001 From: Alissa Date: Mon, 3 Apr 2023 18:33:50 +0300 Subject: [PATCH 19/32] pass over from droppable as well (more useful) --- packages/sortable/src/hooks/useSortable.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/sortable/src/hooks/useSortable.ts b/packages/sortable/src/hooks/useSortable.ts index 06fb4dfb..2f1c34f2 100644 --- a/packages/sortable/src/hooks/useSortable.ts +++ b/packages/sortable/src/hooks/useSortable.ts @@ -68,6 +68,7 @@ export function useSortable({ rect, node, isOver, + over: droppableOver, setNodeRef: setDroppableNodeRef, } = useDroppable({ id, @@ -86,9 +87,9 @@ export function useSortable({ setNodeRef: setDraggableNodeRef, listeners, isDragging, - over, setActivatorNodeRef, transform, + over: draggableOver, } = useDraggable({ id, data, @@ -153,7 +154,7 @@ export function useSortable({ isDragging, listeners, node, - over, + over: droppableOver || draggableOver, setNodeRef, setActivatorNodeRef, setDroppableNodeRef, From 33bf5e1a321ccb22a11ac56cc4e9bcc578aa28b5 Mon Sep 17 00:00:00 2001 From: Alissa Date: Mon, 3 Apr 2023 18:33:59 +0300 Subject: [PATCH 20/32] fix pages story --- stories/3 - Examples/Advanced/Pages/Pages.tsx | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/stories/3 - Examples/Advanced/Pages/Pages.tsx b/stories/3 - Examples/Advanced/Pages/Pages.tsx index 10a4ccf8..a9176515 100644 --- a/stories/3 - Examples/Advanced/Pages/Pages.tsx +++ b/stories/3 - Examples/Advanced/Pages/Pages.tsx @@ -23,6 +23,7 @@ import { useSortable, SortableContext, sortableKeyboardCoordinates, + SortingStrategy, } from '@dnd-kit/sortable'; import {CSS, isKeyboardEvent} from '@dnd-kit/utilities'; import classNames from 'classnames'; @@ -33,6 +34,7 @@ import {Page, Layout, Position} from './Page'; import type {Props as PageProps} from './Page'; import styles from './Pages.module.css'; import pageStyles from './Page.module.css'; +import type {NewIndexGetter} from 'packages/sortable/dist'; interface Props { layout: Layout; @@ -65,6 +67,19 @@ const dropAnimation: DropAnimation = { }), }; +const strategy: SortingStrategy = () => { + return { + scaleX: 1, + scaleY: 1, + x: 0, + y: 0, + }; +}; + +const getNewIndex: NewIndexGetter = ({id, items}) => { + return items.indexOf(id); +}; + export function Pages({layout}: Props) { const [activeId, setActiveId] = useState(null); const [items, setItems] = useState(() => @@ -85,7 +100,11 @@ export function Pages({layout}: Props) { collisionDetection={closestCenter} measuring={measuring} > - +
    {items.map((id, index) => ( ); } - -function always() { - return true; -} From 67e219c19f4a3e0e6bea6519acaf3b220adeda9c Mon Sep 17 00:00:00 2001 From: Alissa Date: Mon, 3 Apr 2023 18:50:42 +0300 Subject: [PATCH 21/32] fix drawer example --- stories/3 - Examples/Drawer/DropRegions.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/stories/3 - Examples/Drawer/DropRegions.tsx b/stories/3 - Examples/Drawer/DropRegions.tsx index ba6a00b5..669af308 100644 --- a/stories/3 - Examples/Drawer/DropRegions.tsx +++ b/stories/3 - Examples/Drawer/DropRegions.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import {useDroppable} from '@dnd-kit/core'; +import {useDndContext, useDroppable} from '@dnd-kit/core'; import {Region} from './constants'; import styles from './Drawer.module.css'; export function DropRegions() { - const {active, setNodeRef: setExpandRegionNodeRef} = useDroppable({ + const {active} = useDndContext(); + const {setNodeRef: setExpandRegionNodeRef} = useDroppable({ id: Region.Expand, }); const {setNodeRef: setCollapseRegionRef} = useDroppable({ From d2d5043e28ebc1f8a48b2006e4b0197acca7b571 Mon Sep 17 00:00:00 2001 From: Alissa Date: Mon, 3 Apr 2023 19:03:29 +0300 Subject: [PATCH 22/32] fix type in switch --- stories/3 - Examples/FormElements/Switch/Switch.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/stories/3 - Examples/FormElements/Switch/Switch.tsx b/stories/3 - Examples/FormElements/Switch/Switch.tsx index 3cdc91a7..c41199ec 100644 --- a/stories/3 - Examples/FormElements/Switch/Switch.tsx +++ b/stories/3 - Examples/FormElements/Switch/Switch.tsx @@ -19,6 +19,7 @@ import {State} from './constants'; import {Thumb} from './Thumb'; import {Track} from './Track'; import styles from './Switch.module.css'; +import type {UniqueIdentifier} from 'packages/core/dist'; export interface Props { accessibilityLabel?: string; @@ -44,7 +45,7 @@ export function Switch({ checked, onChange, }: Props) { - const [overId, setOverId] = useState(null); + const [overId, setOverId] = useState(null); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { @@ -104,7 +105,7 @@ export function Switch({ } function handleDragOver({over}: DragOverEvent) { - setOverId(over?.id ?? null); + setOverId(over?.id || null); } function handleDragEnd({over}: DragEndEvent) { From f79a6e19db8250df2fd71ce62fa5477b7ea8e0b8 Mon Sep 17 00:00:00 2001 From: Alissa Date: Mon, 3 Apr 2023 19:54:43 +0300 Subject: [PATCH 23/32] types and styling in stories --- stories/2 - Presets/Sortable/FramerMotion.tsx | 15 +++++---------- stories/3 - Examples/Drawer/Sheet.tsx | 13 ++++--------- stories/3 - Examples/Tree/SortableTree.tsx | 2 +- .../Tree/components/TreeItem/TreeItem.tsx | 3 ++- 4 files changed, 12 insertions(+), 21 deletions(-) diff --git a/stories/2 - Presets/Sortable/FramerMotion.tsx b/stories/2 - Presets/Sortable/FramerMotion.tsx index 0dc0f88f..34391c38 100644 --- a/stories/2 - Presets/Sortable/FramerMotion.tsx +++ b/stories/2 - Presets/Sortable/FramerMotion.tsx @@ -76,16 +76,11 @@ const initialStyles = { }; function Item({id}: {id: UniqueIdentifier}) { - const { - attributes, - setNodeRef, - listeners, - transform, - isDragging, - } = useSortable({ - id, - transition: null, - }); + const {attributes, setNodeRef, listeners, transform, isDragging} = + useSortable({ + id, + transition: null, + }); return ( { const flattenedTree = flattenTree(items); - const collapsedItems = flattenedTree.reduce( + const collapsedItems = flattenedTree.reduce( (acc, {children, collapsed, id}) => collapsed && children.length ? [...acc, id] : acc, [] diff --git a/stories/3 - Examples/Tree/components/TreeItem/TreeItem.tsx b/stories/3 - Examples/Tree/components/TreeItem/TreeItem.tsx index 5cbac363..dd64caff 100644 --- a/stories/3 - Examples/Tree/components/TreeItem/TreeItem.tsx +++ b/stories/3 - Examples/Tree/components/TreeItem/TreeItem.tsx @@ -3,6 +3,7 @@ import classNames from 'classnames'; import {Action, Handle, Remove} from '../../../../components'; import styles from './TreeItem.module.css'; +import type {UniqueIdentifier} from '@dnd-kit/core'; export interface Props extends Omit, 'id'> { childCount?: number; @@ -15,7 +16,7 @@ export interface Props extends Omit, 'id'> { handleProps?: any; indicator?: boolean; indentationWidth: number; - value: string; + value: UniqueIdentifier; onCollapse?(): void; onRemove?(): void; wrapperRef?(node: HTMLLIElement): void; From dc114ecc250b31eb3f719f83686c0a40093bed0b Mon Sep 17 00:00:00 2001 From: Alissa Date: Tue, 4 Apr 2023 08:06:28 +0300 Subject: [PATCH 24/32] add active for over item --- packages/core/src/components/DndContext/DndContext.tsx | 1 + .../core/src/components/DndContext/activeAndOverAPI.ts | 6 ++++++ packages/core/src/hooks/useDroppable.ts | 10 ++++++++-- packages/core/src/store/context.ts | 1 + packages/core/src/store/types.ts | 1 + packages/sortable/src/hooks/useSortable.ts | 3 ++- 6 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/core/src/components/DndContext/DndContext.tsx b/packages/core/src/components/DndContext/DndContext.tsx index 7966a2b0..0b3885fa 100644 --- a/packages/core/src/components/DndContext/DndContext.tsx +++ b/packages/core/src/components/DndContext/DndContext.tsx @@ -687,6 +687,7 @@ export const DndContext = memo(function DndContext({ useMyOverForDroppable: activeAndOverAPI.useMyOverForDroppable, measureDroppableContainers, isDefaultContext: false, + useMyActiveForDroppable: activeAndOverAPI.useMyActiveForDroppable, }; return context; diff --git a/packages/core/src/components/DndContext/activeAndOverAPI.ts b/packages/core/src/components/DndContext/activeAndOverAPI.ts index 9e295c86..bfd37b3d 100644 --- a/packages/core/src/components/DndContext/activeAndOverAPI.ts +++ b/packages/core/src/components/DndContext/activeAndOverAPI.ts @@ -76,6 +76,12 @@ export function createActiveAndOverAPI(rect: Rects) { ); }, + useMyActiveForDroppable: function (droppableId: UniqueIdentifier) { + return useSyncExternalStore(subscribe, () => { + return over && over.id === droppableId ? active : null; + }); + }, + useActivatorEvent: function () { return useSyncExternalStore(subscribe, () => activatorEvent); }, diff --git a/packages/core/src/hooks/useDroppable.ts b/packages/core/src/hooks/useDroppable.ts index 05a1366a..6ecfacc6 100644 --- a/packages/core/src/hooks/useDroppable.ts +++ b/packages/core/src/hooks/useDroppable.ts @@ -43,9 +43,14 @@ export function useDroppable({ resizeObserverConfig, }: UseDroppableArguments) { const key = useUniqueId(ID_PREFIX); - const {dispatch, useMyOverForDroppable, measureDroppableContainers} = - useContext(InternalContext); + const { + dispatch, + useMyOverForDroppable, + measureDroppableContainers, + useMyActiveForDroppable, + } = useContext(InternalContext); const over = useMyOverForDroppable(id); + const activeOverItem = useMyActiveForDroppable(id); const previous = useRef({disabled}); const resizeObserverConnected = useRef(false); const rect = useRef(null); @@ -158,6 +163,7 @@ export function useDroppable({ return { //I removed the active from here, it forces all droppable to re-render when active changes. + activeOverItem, rect, isOver: !!over, node: nodeRef, diff --git a/packages/core/src/store/context.ts b/packages/core/src/store/context.ts index 83ae683e..24ba2312 100644 --- a/packages/core/src/store/context.ts +++ b/packages/core/src/store/context.ts @@ -44,6 +44,7 @@ export const defaultInternalContext: InternalContextDescriptor = { draggableNodes: new Map(), measureDroppableContainers: noop, useMyActive: () => null, + useMyActiveForDroppable: () => null, useGloablActive: () => null, useMyActivatorEvent: () => null, useGlobalActivatorEvent: () => null, diff --git a/packages/core/src/store/types.ts b/packages/core/src/store/types.ts index 333d588f..d68bd612 100644 --- a/packages/core/src/store/types.ts +++ b/packages/core/src/store/types.ts @@ -102,6 +102,7 @@ export interface InternalContextDescriptor { activators: SyntheticListeners; useMyActive: (id: UniqueIdentifier) => Active | null; useGloablActive: () => Active | null; + useMyActiveForDroppable: (droppableId: UniqueIdentifier) => Active | null; useMyActivatorEvent: (id: UniqueIdentifier) => Event | null; useGlobalActivatorEvent: () => Event | null; useMyActiveNodeRect: (id: UniqueIdentifier) => ClientRect | null; diff --git a/packages/sortable/src/hooks/useSortable.ts b/packages/sortable/src/hooks/useSortable.ts index 2f1c34f2..92c1d976 100644 --- a/packages/sortable/src/hooks/useSortable.ts +++ b/packages/sortable/src/hooks/useSortable.ts @@ -70,6 +70,7 @@ export function useSortable({ isOver, over: droppableOver, setNodeRef: setDroppableNodeRef, + activeOverItem, } = useDroppable({ id, data, @@ -143,7 +144,7 @@ export function useSortable({ }, [newIndex]); return { - active, + active: active || activeOverItem, attributes, data, rect, From 1c7be362fe1d5e9827f643bf2150094159e40ff4 Mon Sep 17 00:00:00 2001 From: Alissa Date: Tue, 4 Apr 2023 08:48:22 +0300 Subject: [PATCH 25/32] cache prev items and container in item and expose global active ref internally --- .../src/components/SortableContext.tsx | 18 +++---- .../src/components/useGlobalActiveRef.ts | 30 +++++++++++ .../src/components/usePreviousSortingState.ts | 42 ---------------- packages/sortable/src/hooks/defaults.ts | 4 +- packages/sortable/src/hooks/useSortable.ts | 40 +++++++++------ .../Sortable/MultipleContainers.tsx | 50 +++++++++---------- stories/3 - Examples/Advanced/Pages/Pages.tsx | 6 +++ 7 files changed, 93 insertions(+), 97 deletions(-) create mode 100644 packages/sortable/src/components/useGlobalActiveRef.ts delete mode 100644 packages/sortable/src/components/usePreviousSortingState.ts diff --git a/packages/sortable/src/components/SortableContext.tsx b/packages/sortable/src/components/SortableContext.tsx index d34bb9c5..7aeef804 100644 --- a/packages/sortable/src/components/SortableContext.tsx +++ b/packages/sortable/src/components/SortableContext.tsx @@ -6,7 +6,7 @@ import type {Disabled, NewIndexGetter, SortingStrategy} from '../types'; import {normalizeDisabled} from '../utilities'; import {rectSortingStrategy} from '../strategies'; import {createSortingAPI} from './sortingAPI'; -import {usePreviousSortingStateRef} from './usePreviousSortingState'; +import {useGlobalActiveRef} from './useGlobalActiveRef'; import {defaultNewIndexGetter} from '../hooks/defaults'; export interface Props { @@ -27,7 +27,7 @@ interface ContextDescriptor { items: UniqueIdentifier[]; useDragOverlay: boolean; useMyNewIndex: (id: UniqueIdentifier, currentIndex: number) => number; - previousSortingStateRef: ReturnType; + globalActiveRef: ReturnType; useMyStrategyValue: ( id: UniqueIdentifier, currentIndex: number, @@ -45,8 +45,8 @@ export const Context = React.createContext({ droppable: false, }, useMyNewIndex: () => -1, - previousSortingStateRef: { - current: {activeId: null, containerId: '', items: []}, + globalActiveRef: { + current: {activeId: null, prevActiveId: null}, }, useMyStrategyValue: () => null, }); @@ -102,11 +102,7 @@ export function SortableContext({ previousItemsRef.current = items; }, [items]); - const previousSortingStateRef = usePreviousSortingStateRef({ - activeId: active?.id || null, - containerId, - items, - }); + const globalActiveRef = useGlobalActiveRef(active?.id || null); const contextValue = useMemo( (): ContextDescriptor => ({ containerId, @@ -115,7 +111,7 @@ export function SortableContext({ items, useDragOverlay, useMyNewIndex: sortingAPI.useMyNewIndex, - previousSortingStateRef, + globalActiveRef, useMyStrategyValue: sortingAPI.useMyStrategyValue, }), // eslint-disable-next-line react-hooks/exhaustive-deps @@ -127,7 +123,7 @@ export function SortableContext({ items, useDragOverlay, sortingAPI, - previousSortingStateRef, + globalActiveRef, ] ); diff --git a/packages/sortable/src/components/useGlobalActiveRef.ts b/packages/sortable/src/components/useGlobalActiveRef.ts new file mode 100644 index 00000000..008ab42c --- /dev/null +++ b/packages/sortable/src/components/useGlobalActiveRef.ts @@ -0,0 +1,30 @@ +import type {UniqueIdentifier} from '@dnd-kit/core'; +import {useRef, useEffect} from 'react'; + +export function useGlobalActiveRef(activeId: UniqueIdentifier | null) { + const activeState = useRef<{ + activeId: null | UniqueIdentifier; + prevActiveId: null | UniqueIdentifier; + }>({activeId: null, prevActiveId: null}); + + activeState.current.activeId = activeId; + + useEffect(() => { + if (activeId === activeState.current.prevActiveId) { + return; + } + + if (activeId && !activeState.current.prevActiveId) { + activeState.current.prevActiveId = activeId; + return; + } + + const timeoutId = setTimeout(() => { + activeState.current.prevActiveId = activeId; + }, 50); + + return () => clearTimeout(timeoutId); + }, [activeId]); + + return activeState; +} diff --git a/packages/sortable/src/components/usePreviousSortingState.ts b/packages/sortable/src/components/usePreviousSortingState.ts deleted file mode 100644 index 54150b30..00000000 --- a/packages/sortable/src/components/usePreviousSortingState.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type {UniqueIdentifier} from '@dnd-kit/core'; -import {useRef, useEffect} from 'react'; -export type SortableState = { - activeId: UniqueIdentifier | null; - items: UniqueIdentifier[]; - containerId: string; -}; - -export function usePreviousSortingStateRef(sortingState: SortableState) { - const previous = useRef(sortingState); - - const activeId = sortingState.activeId; - useEffect(() => { - if (activeId === previous.current.activeId) { - return; - } - - if (activeId && !previous.current.activeId) { - previous.current.activeId = activeId; - return; - } - - const timeoutId = setTimeout(() => { - previous.current.activeId = activeId; - }, 50); - - return () => clearTimeout(timeoutId); - }, [activeId]); - - const {items, containerId} = sortingState; - useEffect(() => { - if (containerId !== previous.current.containerId) { - previous.current.containerId = containerId; - } - - if (items !== previous.current.items) { - previous.current.items = items; - } - }, [activeId, containerId, items]); - - return previous; -} diff --git a/packages/sortable/src/hooks/defaults.ts b/packages/sortable/src/hooks/defaults.ts index bb1df92a..42157429 100644 --- a/packages/sortable/src/hooks/defaults.ts +++ b/packages/sortable/src/hooks/defaults.ts @@ -14,7 +14,6 @@ export const defaultNewIndexGetter: NewIndexGetter = ({ export const defaultAnimateLayoutChanges: AnimateLayoutChanges = ({ containerId, - isDragging, wasDragging, index, items, @@ -22,6 +21,7 @@ export const defaultAnimateLayoutChanges: AnimateLayoutChanges = ({ previousItems, previousContainerId, transition, + isSorting, }) => { if (!transition || !wasDragging) { return false; @@ -31,7 +31,7 @@ export const defaultAnimateLayoutChanges: AnimateLayoutChanges = ({ return false; } - if (isDragging) { + if (isSorting) { return true; } diff --git a/packages/sortable/src/hooks/useSortable.ts b/packages/sortable/src/hooks/useSortable.ts index 92c1d976..77c47d84 100644 --- a/packages/sortable/src/hooks/useSortable.ts +++ b/packages/sortable/src/hooks/useSortable.ts @@ -47,7 +47,7 @@ export function useSortable({ disableTransforms, useDragOverlay, useMyNewIndex, - previousSortingStateRef, + globalActiveRef, useMyStrategyValue, } = useContext(Context); const disabled: Disabled = normalizeLocalDisabled( @@ -112,22 +112,25 @@ export function useSortable({ (otherItemDisplacement ? JSON.parse(otherItemDisplacement) : null); const newIndex = useMyNewIndex(id, index); - const prevNewIndex = useRef(newIndex); - const itemsHaveChanged = items !== previousSortingStateRef.current.items; + const prevItemState = useRef({ + newIndex, + items, + containerId, + }); + const itemsHaveChanged = items !== prevItemState.current.items; const shouldAnimateLayoutChanges = animateLayoutChanges({ active, containerId, isDragging, - //this value was true as long as there is an active item... not sure what was the purpose - isSorting: true, + isSorting: globalActiveRef.current.activeId != null, id, index, items, - newIndex: prevNewIndex.current, - previousItems: previousSortingStateRef.current.items, - previousContainerId: previousSortingStateRef.current.containerId, + newIndex: prevItemState.current.newIndex, + previousItems: prevItemState.current.items, + previousContainerId: prevItemState.current.containerId, transition, - wasDragging: previousSortingStateRef.current.activeId != null, + wasDragging: globalActiveRef.current.prevActiveId != null, }); const derivedTransform = useDerivedTransform({ @@ -138,10 +141,17 @@ export function useSortable({ }); useEffect(() => { - if (prevNewIndex.current !== newIndex) { - prevNewIndex.current = newIndex; + if (prevItemState.current.newIndex !== newIndex) { + prevItemState.current.newIndex = newIndex; + } + if (containerId !== prevItemState.current.containerId) { + prevItemState.current.containerId = containerId; + } + + if (items !== prevItemState.current.items) { + prevItemState.current.items = items; } - }, [newIndex]); + }, [containerId, items, newIndex]); return { active: active || activeOverItem, @@ -169,7 +179,7 @@ export function useSortable({ // Temporarily disable transitions for a single frame to set up derived transforms derivedTransform || // Or to prevent items jumping to back to their "new" position when items change - (itemsHaveChanged && prevNewIndex.current === index) + (itemsHaveChanged && prevItemState.current.newIndex === index) ) { return disabledTransition; } @@ -181,9 +191,7 @@ export function useSortable({ return undefined; } - const isDropping = - previousSortingStateRef.current.activeId === id && !isDragging; - if (shouldAnimateLayoutChanges || !isDropping) { + if (globalActiveRef.current.activeId || shouldAnimateLayoutChanges) { return CSS.Transition.toString({ ...transition, property: transitionProperty, diff --git a/stories/2 - Presets/Sortable/MultipleContainers.tsx b/stories/2 - Presets/Sortable/MultipleContainers.tsx index 23a9dca9..7ca06f9b 100644 --- a/stories/2 - Presets/Sortable/MultipleContainers.tsx +++ b/stories/2 - Presets/Sortable/MultipleContainers.tsx @@ -53,34 +53,25 @@ function DroppableContainer({ id, items, style, + isActiveOverContainer, ...props }: ContainerProps & { disabled?: boolean; id: UniqueIdentifier; items: UniqueIdentifier[]; style?: React.CSSProperties; + isActiveOverContainer: boolean; }) { - const { - active, - attributes, - isDragging, - listeners, - over, - setNodeRef, - transition, - transform, - } = useSortable({ - id, - data: { - type: 'container', - children: items, - }, - animateLayoutChanges, - }); - const isOverContainer = over - ? (id === over.id && active?.data.current?.type !== 'container') || - items.includes(over.id) - : false; + const {attributes, isDragging, listeners, setNodeRef, transition, transform} = + useSortable({ + id, + data: { + type: 'container', + children: items, + }, + animateLayoutChanges, + }); + const isOverContainer = isActiveOverContainer; return ( ( + null + ); const [activeId, setActiveId] = useState(null); const lastOverId = useRef(null); const recentlyMovedToNewContainer = useRef(false); @@ -283,6 +277,7 @@ export function MultipleContainers({ }; const onDragCancel = () => { + setOverContainer(null); if (clonedItems) { // Reset items to their original state in case items have been // Dragged across containers @@ -323,8 +318,10 @@ export function MultipleContainers({ const activeContainer = findContainer(active.id); if (!overContainer || !activeContainer) { + setOverContainer(null); return; } + setOverContainer(overContainer); if (activeContainer !== overContainer) { setItems((items) => { @@ -370,6 +367,7 @@ export function MultipleContainers({ } }} onDragEnd={({active, over}) => { + setOverContainer(null); if (active.id in items && over?.id) { setContainers((containers) => { const activeIndex = containers.indexOf(active.id); @@ -472,6 +470,7 @@ export function MultipleContainers({ style={containerStyle} unstyled={minimal} onRemove={() => handleRemove(containerId)} + isActiveOverContainer={overContainer === containerId} > {items[containerId].map((value, index) => { @@ -500,6 +499,7 @@ export function MultipleContainers({ items={empty} onClick={handleAddColumn} placeholder + isActiveOverContainer={false} > + Add column @@ -675,9 +675,7 @@ function SortableItem({ setActivatorNodeRef, listeners, isDragging, - isSorting, over, - overIndex, transform, transition, } = useSortable({ @@ -691,7 +689,7 @@ function SortableItem({ ref={disabled ? undefined : setNodeRef} value={id} dragging={isDragging} - sorting={isSorting} + sorting={true} handle={handle} handleProps={handle ? {ref: setActivatorNodeRef} : undefined} index={index} @@ -700,8 +698,8 @@ function SortableItem({ index, value: id, isDragging, - isSorting, - overIndex: over ? getIndex(over.id) : overIndex, + isSorting: true, + overIndex: over ? getIndex(over.id) : -1, containerId, })} color={getColor(id)} diff --git a/stories/3 - Examples/Advanced/Pages/Pages.tsx b/stories/3 - Examples/Advanced/Pages/Pages.tsx index a9176515..cf7bc684 100644 --- a/stories/3 - Examples/Advanced/Pages/Pages.tsx +++ b/stories/3 - Examples/Advanced/Pages/Pages.tsx @@ -24,6 +24,7 @@ import { SortableContext, sortableKeyboardCoordinates, SortingStrategy, + AnimateLayoutChanges, } from '@dnd-kit/sortable'; import {CSS, isKeyboardEvent} from '@dnd-kit/utilities'; import classNames from 'classnames'; @@ -193,6 +194,7 @@ function SortablePage({ transition, } = useSortable({ id, + animateLayoutChanges: always, }); return ( @@ -217,3 +219,7 @@ function SortablePage({ /> ); } + +const always: AnimateLayoutChanges = () => { + return true; +}; From 90d0baf5cd8fe2254b2fe9fcf1163c898f9b13c4 Mon Sep 17 00:00:00 2001 From: Alissa Date: Tue, 4 Apr 2023 10:05:41 +0300 Subject: [PATCH 26/32] add useConditionalDndContext --- packages/core/src/hooks/index.ts | 2 +- packages/core/src/hooks/useDndContext.ts | 13 +++++++++++-- packages/core/src/index.ts | 1 + .../2 - Presets/Sortable/1-Vertical.story.tsx | 1 + stories/2 - Presets/Sortable/Sortable.tsx | 17 +++++++++++++++-- 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/packages/core/src/hooks/index.ts b/packages/core/src/hooks/index.ts index 4ee9dda6..43c7030f 100644 --- a/packages/core/src/hooks/index.ts +++ b/packages/core/src/hooks/index.ts @@ -4,7 +4,7 @@ export type { DraggableSyntheticListeners, UseDraggableArguments, } from './useDraggable'; -export {useDndContext} from './useDndContext'; +export {useDndContext, useConditionalDndContext} from './useDndContext'; export type {UseDndContextReturnValue} from './useDndContext'; export {useDroppable} from './useDroppable'; export type {UseDroppableArguments} from './useDroppable'; diff --git a/packages/core/src/hooks/useDndContext.ts b/packages/core/src/hooks/useDndContext.ts index 97cfbfea..87c29cf6 100644 --- a/packages/core/src/hooks/useDndContext.ts +++ b/packages/core/src/hooks/useDndContext.ts @@ -1,8 +1,17 @@ -import {ContextType, useContext} from 'react'; -import {PublicContext} from '../store'; +import {ContextType, createContext, useContext} from 'react'; +import {PublicContext, PublicContextDescriptor} from '../store'; export function useDndContext() { return useContext(PublicContext); } +const NullContext = createContext(null); + +export function useConditionalDndContext( + condition: boolean +): PublicContextDescriptor | null { + return useContext( + condition ? PublicContext : NullContext + ) as PublicContextDescriptor | null; +} export type UseDndContextReturnValue = ContextType; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 304f8b1a..f0594939 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -32,6 +32,7 @@ export { useDraggable, useDndContext, useDroppable, + useConditionalDndContext, } from './hooks'; export type { AutoScrollOptions, diff --git a/stories/2 - Presets/Sortable/1-Vertical.story.tsx b/stories/2 - Presets/Sortable/1-Vertical.story.tsx index 8bba0a90..7e5b9edd 100644 --- a/stories/2 - Presets/Sortable/1-Vertical.story.tsx +++ b/stories/2 - Presets/Sortable/1-Vertical.story.tsx @@ -141,6 +141,7 @@ export const RerenderBeforeSorting = () => { return ( { return { height: active ? 100 : 80, diff --git a/stories/2 - Presets/Sortable/Sortable.tsx b/stories/2 - Presets/Sortable/Sortable.tsx index 2d492aa7..90000f7a 100644 --- a/stories/2 - Presets/Sortable/Sortable.tsx +++ b/stories/2 - Presets/Sortable/Sortable.tsx @@ -22,6 +22,7 @@ import { useSensors, defaultDropAnimationSideEffects, useDndContext, + useConditionalDndContext, } from '@dnd-kit/core'; import { arrayMove, @@ -72,6 +73,7 @@ export interface Props { id: UniqueIdentifier; }): React.CSSProperties; isDisabled?(id: UniqueIdentifier): boolean; + usingGlobalActiveInStyle?: boolean; } const dropAnimationConfig: DropAnimation = { @@ -115,6 +117,7 @@ export function Sortable({ style, useDragOverlay = true, wrapperStyle = () => ({}), + usingGlobalActiveInStyle = false, }: Props) { const [items, setItems] = useState( () => @@ -225,6 +228,7 @@ export function Sortable({ onRemove={handleRemove} animateLayoutChanges={animateLayoutChanges} useDragOverlay={useDragOverlay} + usingGlobalActiveInStyle={usingGlobalActiveInStyle} /> ))} @@ -302,6 +306,7 @@ interface SortableItemProps { style(values: any): React.CSSProperties; renderItem?(args: any): React.ReactElement; wrapperStyle: Props['wrapperStyle']; + usingGlobalActiveInStyle: boolean; } export function SortableItem({ @@ -315,6 +320,7 @@ export function SortableItem({ renderItem, useDragOverlay, wrapperStyle, + usingGlobalActiveInStyle, }: SortableItemProps) { const { active, @@ -331,6 +337,8 @@ export function SortableItem({ disabled, }); + const dndContext = useConditionalDndContext(usingGlobalActiveInStyle); + return ( onRemove(id) : undefined} transform={transform} transition={transition} - wrapperStyle={wrapperStyle?.({index, isDragging, active, id})} + wrapperStyle={wrapperStyle?.({ + index, + isDragging, + active: dndContext?.active || active, + id, + })} listeners={listeners} data-index={index} data-id={id} From a19a3e2323b691cb841161a6f4204aba1ea4d88b Mon Sep 17 00:00:00 2001 From: Alissa Date: Tue, 4 Apr 2023 10:29:56 +0300 Subject: [PATCH 27/32] add test for render only needed sortable items --- cypress/integration/sortable_spec.ts | 27 +++++++++++++++++++ .../Sortable/SortableRenders.story.tsx | 19 +++++-------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/cypress/integration/sortable_spec.ts b/cypress/integration/sortable_spec.ts index 9e9f398d..1dd847bd 100644 --- a/cypress/integration/sortable_spec.ts +++ b/cypress/integration/sortable_spec.ts @@ -543,3 +543,30 @@ describe('Sortable Virtualized List', () => { }); }); }); + +describe('Sortable Renders only what is necessary ', () => { + it('should render active and items between active and over', () => { + cy.visitStory('presets-sortable-renders--basic-setup'); + + cy.get('[data-cypress="draggable-item"]').then((droppables) => { + const coords = droppables[1].getBoundingClientRect(); //drop after item id - 3 + return cy + .findFirstDraggableItem() + .mouseMoveBy(coords.x + 10, coords.y + 10, {delay: 1, noDrop: true}); + }); + + for (let id = 1; id <= 3; id++) { + cy.get(`[data-testid="sortable-status-${id}"]`).should( + 'have.text', + `updated ${id}` + ); + } + + for (let id = 4; id <= 10; id++) { + cy.get(`[data-testid="sortable-status-${id}"]`).should( + 'have.text', + `mounted ${id}` + ); + } + }); +}); diff --git a/stories/2 - Presets/Sortable/SortableRenders.story.tsx b/stories/2 - Presets/Sortable/SortableRenders.story.tsx index 1246ed2e..a0af42f3 100644 --- a/stories/2 - Presets/Sortable/SortableRenders.story.tsx +++ b/stories/2 - Presets/Sortable/SortableRenders.story.tsx @@ -16,17 +16,10 @@ export default { }; function SortableItem({id, index}: {id: UniqueIdentifier; index: number}) { - const { - attributes, - isDragging, - isSorting, - listeners, - setNodeRef, - transform, - transition, - } = useSortable({ - id, - }); + const {attributes, isDragging, listeners, setNodeRef, transform, transition} = + useSortable({ + id, + }); const span = useRef(null); @@ -40,14 +33,14 @@ function SortableItem({id, index}: {id: UniqueIdentifier; index: number}) { }} >
    - + mounted {id} Date: Wed, 5 Apr 2023 08:05:52 +0300 Subject: [PATCH 28/32] fix strategy delta diffs and pass disable transforms only for active item --- .../src/components/SortableContext.tsx | 10 ++++----- .../sortable/src/components/sortingAPI.tsx | 22 ++++++++++++++++++- packages/sortable/src/hooks/useSortable.ts | 5 +++-- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/sortable/src/components/SortableContext.tsx b/packages/sortable/src/components/SortableContext.tsx index 7aeef804..37e84888 100644 --- a/packages/sortable/src/components/SortableContext.tsx +++ b/packages/sortable/src/components/SortableContext.tsx @@ -23,7 +23,6 @@ const ID_PREFIX = 'Sortable'; interface ContextDescriptor { containerId: string; disabled: Disabled; - disableTransforms: boolean; items: UniqueIdentifier[]; useDragOverlay: boolean; useMyNewIndex: (id: UniqueIdentifier, currentIndex: number) => number; @@ -33,11 +32,11 @@ interface ContextDescriptor { currentIndex: number, activeNodeRect: ClientRect | null ) => string | null; + useShouldUseDragTransform: (id: UniqueIdentifier) => boolean; } export const Context = React.createContext({ containerId: ID_PREFIX, - disableTransforms: false, items: [], useDragOverlay: false, disabled: { @@ -49,6 +48,7 @@ export const Context = React.createContext({ current: {activeId: null, prevActiveId: null}, }, useMyStrategyValue: () => null, + useShouldUseDragTransform: () => false, }); export function SortableContext({ @@ -89,7 +89,6 @@ export function SortableContext({ const isDragging = active != null; const previousItemsRef = useRef(items); const itemsHaveChanged = sortingAPI.getItemsHaveChanged(); - const disableTransforms = !sortingAPI.getShouldDisplaceItems(); const disabled = normalizeDisabled(disabledProp); useIsomorphicLayoutEffect(() => { @@ -107,7 +106,7 @@ export function SortableContext({ (): ContextDescriptor => ({ containerId, disabled, - disableTransforms, + useShouldUseDragTransform: sortingAPI.useShouldUseDragTransform, items, useDragOverlay, useMyNewIndex: sortingAPI.useMyNewIndex, @@ -119,8 +118,7 @@ export function SortableContext({ containerId, disabled.draggable, disabled.droppable, - disableTransforms, - items, + // items, useDragOverlay, sortingAPI, globalActiveRef, diff --git a/packages/sortable/src/components/sortingAPI.tsx b/packages/sortable/src/components/sortingAPI.tsx index 60c9912d..97b98bf4 100644 --- a/packages/sortable/src/components/sortingAPI.tsx +++ b/packages/sortable/src/components/sortingAPI.tsx @@ -31,6 +31,8 @@ export function createSortingAPI( }; } + const nullDelta = JSON.stringify({x: 0, y: 0, scaleX: 1, scaleY: 1}); + function calculateIndexes() { const active = activeAndOverAPI.getActive(); if (!active) { @@ -99,8 +101,26 @@ export function createSortingAPI( overIndex, index: currentIndex, }); + if (!delta) { + return null; + } - return JSON.stringify(delta); + // We need to stringify the delta object to compare it to the previous + //we construct a new object so the order of keys will be the same + const deltaJson = JSON.stringify({ + x: delta.x, + y: delta.y, + scaleX: delta.scaleX, + scaleY: delta.scaleY, + }); + return deltaJson === nullDelta ? null : deltaJson; + }); + }, + useShouldUseDragTransform: (id: UniqueIdentifier) => { + return useSyncExternalStore(subscribe, () => { + return id === activeAndOverAPI.getActive()?.id + ? shouldDisplaceItems() + : false; }); }, getOverIndex: () => overIndex, diff --git a/packages/sortable/src/hooks/useSortable.ts b/packages/sortable/src/hooks/useSortable.ts index 77c47d84..13f846ca 100644 --- a/packages/sortable/src/hooks/useSortable.ts +++ b/packages/sortable/src/hooks/useSortable.ts @@ -44,7 +44,7 @@ export function useSortable({ items, containerId, disabled: globalDisabled, - disableTransforms, + useShouldUseDragTransform, useDragOverlay, useMyNewIndex, globalActiveRef, @@ -103,8 +103,9 @@ export function useSortable({ const setNodeRef = useCombinedRefs(setDroppableNodeRef, setDraggableNodeRef); + const shouldUseDragTransforms = useShouldUseDragTransform(id); const shouldDisplaceDragSource = - !disableTransforms && !useDragOverlay && isDragging; + shouldUseDragTransforms && !useDragOverlay && isDragging; const dragSourceDisplacement = shouldDisplaceDragSource ? transform : null; const otherItemDisplacement = useMyStrategyValue(id, index, activeNodeRect); const finalTransform = From baf7e7e0a111457a667303619dbfe277cbecae9e Mon Sep 17 00:00:00 2001 From: Alissa Date: Wed, 5 Apr 2023 08:31:54 +0300 Subject: [PATCH 29/32] add tests for drop + memo items in test --- cypress/integration/droppable_spec.ts | 68 +++++++++++++++++-- cypress/integration/sortable_spec.ts | 24 ++++++- .../Droppable/DroppableRenders.story.tsx | 13 ++-- .../Sortable/SortableRenders.story.tsx | 6 +- 4 files changed, 98 insertions(+), 13 deletions(-) diff --git a/cypress/integration/droppable_spec.ts b/cypress/integration/droppable_spec.ts index 4c6dedf1..601ee2fa 100644 --- a/cypress/integration/droppable_spec.ts +++ b/cypress/integration/droppable_spec.ts @@ -2,14 +2,27 @@ describe('Droppable', () => { describe('Droppable Renders', () => { - it('should re-render only the dragged item and the container dragged over', () => { + it('should re-render only the dragged item and the container dragged over - no drop', () => { cy.visitStory('core-droppablerenders-usedroppable--multiple-droppables'); - cy.get('[data-cypress="droppable-container-C"]').then((droppable) => { + cy.get('[data-cypress="droppable-container-A"]').then((droppable) => { const coords = droppable[0].getBoundingClientRect(); return cy - .findFirstDraggableItem() - .mouseMoveBy(coords.x + 10, coords.y + 10, {delay: 1, noDrop: true}); + .get('[data-cypress="draggable-item"]') + .first() + .then((draggable) => { + const initialCoords = draggable[0].getBoundingClientRect(); + return cy + .wrap(draggable, {log: false}) + .mouseMoveBy( + coords.x - initialCoords.x + 10, + coords.y - initialCoords.y + 10, + { + delay: 1000, + noDrop: true, + } + ); + }); }); cy.get('[data-testid="draggable-status-1"]').should( @@ -27,16 +40,61 @@ describe('Droppable', () => { cy.get('[data-testid="droppable-status-A"]').should( 'have.text', - 'mounted' + 'updated' ); cy.get('[data-testid="droppable-status-B"]').should( 'have.text', 'mounted' ); cy.get('[data-testid="droppable-status-C"]').should( + 'have.text', + 'mounted' + ); + }); + + it('should re-render only the dragged item and the container dragged over - with drop', () => { + cy.visitStory('core-droppablerenders-usedroppable--multiple-droppables'); + + cy.get('[data-cypress="droppable-container-A"]').then((droppable) => { + const coords = droppable[0].getBoundingClientRect(); + return cy + .get('[data-cypress="draggable-item"]') + .last() + .then((draggable) => { + const initialCoords = draggable[0].getBoundingClientRect(); + return cy + .wrap(draggable, {log: false}) + .mouseMoveBy( + coords.x - initialCoords.x + 10, + coords.y - initialCoords.y + 10, + { + delay: 1000, + noDrop: false, + } + ); + }); + }); + + //the dropped item is mounted because it moves to a different container + for (let i = 1; i <= 3; i++) { + cy.get(`[data-testid="draggable-status-${i}"]`).should( + 'have.text', + 'mounted' + ); + } + + cy.get('[data-testid="droppable-status-A"]').should( 'have.text', 'updated' ); + cy.get('[data-testid="droppable-status-B"]').should( + 'have.text', + 'mounted' + ); + cy.get('[data-testid="droppable-status-C"]').should( + 'have.text', + 'mounted' + ); }); }); }); diff --git a/cypress/integration/sortable_spec.ts b/cypress/integration/sortable_spec.ts index 1dd847bd..e5844ed7 100644 --- a/cypress/integration/sortable_spec.ts +++ b/cypress/integration/sortable_spec.ts @@ -544,8 +544,8 @@ describe('Sortable Virtualized List', () => { }); }); -describe('Sortable Renders only what is necessary ', () => { - it('should render active and items between active and over', () => { +describe.only('Sortable Renders only what is necessary ', () => { + it('should render active and items between active and over - no drop', () => { cy.visitStory('presets-sortable-renders--basic-setup'); cy.get('[data-cypress="draggable-item"]').then((droppables) => { @@ -569,4 +569,24 @@ describe('Sortable Renders only what is necessary ', () => { ); } }); + + it('should render active only on d&d in place - with drop', () => { + cy.visitStory('presets-sortable-renders--basic-setup'); + + cy.findFirstDraggableItem().mouseMoveBy(10, 10, { + delay: 1, + noDrop: false, + }); + + cy.get(`[data-testid="sortable-status-1"]`).should( + 'have.text', + `updated 1` + ); + for (let id = 2; id <= 10; id++) { + cy.get(`[data-testid="sortable-status-${id}"]`).should( + 'have.text', + `mounted ${id}` + ); + } + }); }); diff --git a/stories/1 - Core/Droppable/DroppableRenders.story.tsx b/stories/1 - Core/Droppable/DroppableRenders.story.tsx index e0d97ad0..94c97119 100644 --- a/stories/1 - Core/Droppable/DroppableRenders.story.tsx +++ b/stories/1 - Core/Droppable/DroppableRenders.story.tsx @@ -29,7 +29,10 @@ interface Props { modifiers?: Modifiers; value?: string; } - +// we memoize the components to filters out the re-renders caused by the parent +// context changes won't be affected by this +const MemoDraggable = React.memo(DraggableItem); +const MemoDroppable = React.memo(Droppable); function DroppableStory({ containers = ['A'], items = ['1'], @@ -75,16 +78,16 @@ function DroppableStory({ {orphanItems.map((itemId) => ( - + ))} {containers.map((id) => ( - + {itemsPyParent[id]?.map((itemId) => ( - + )) || null} - + ))} diff --git a/stories/2 - Presets/Sortable/SortableRenders.story.tsx b/stories/2 - Presets/Sortable/SortableRenders.story.tsx index a0af42f3..064cd5b7 100644 --- a/stories/2 - Presets/Sortable/SortableRenders.story.tsx +++ b/stories/2 - Presets/Sortable/SortableRenders.story.tsx @@ -54,6 +54,10 @@ function SortableItem({id, index}: {id: UniqueIdentifier; index: number}) { ); } +// we memoize the components to filters out the re-renders caused by the parent +// context changes won't be affected by this +const MemoSortableItem = React.memo(SortableItem); + function Sortable() { const [items, setItems] = useState(() => createRange(20, (index) => index + 1) @@ -77,7 +81,7 @@ function Sortable() { {items.map((id, index) => ( - + ))} From 38cd6e93e0832a92f424739e66b454680f1d0235 Mon Sep 17 00:00:00 2001 From: Alissa Date: Wed, 5 Apr 2023 08:32:57 +0300 Subject: [PATCH 30/32] remove only --- cypress/integration/sortable_spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cypress/integration/sortable_spec.ts b/cypress/integration/sortable_spec.ts index e5844ed7..f0241ab3 100644 --- a/cypress/integration/sortable_spec.ts +++ b/cypress/integration/sortable_spec.ts @@ -544,7 +544,7 @@ describe('Sortable Virtualized List', () => { }); }); -describe.only('Sortable Renders only what is necessary ', () => { +describe('Sortable Renders only what is necessary ', () => { it('should render active and items between active and over - no drop', () => { cy.visitStory('presets-sortable-renders--basic-setup'); @@ -570,6 +570,8 @@ describe.only('Sortable Renders only what is necessary ', () => { } }); + //we test for drop in place, because otherwise items change and cause a real re-render to all items + //probably possible to fix that too but I didn't get there it('should render active only on d&d in place - with drop', () => { cy.visitStory('presets-sortable-renders--basic-setup'); From 64a4c7a997bf3501b76060e80f9de0a4f91edfaa Mon Sep 17 00:00:00 2001 From: Alissa Date: Tue, 25 Apr 2023 10:48:22 +0300 Subject: [PATCH 31/32] uncomment items dependency --- packages/sortable/src/components/SortableContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sortable/src/components/SortableContext.tsx b/packages/sortable/src/components/SortableContext.tsx index 37e84888..93355d64 100644 --- a/packages/sortable/src/components/SortableContext.tsx +++ b/packages/sortable/src/components/SortableContext.tsx @@ -118,7 +118,7 @@ export function SortableContext({ containerId, disabled.draggable, disabled.droppable, - // items, + items, useDragOverlay, sortingAPI, globalActiveRef, From 358d37dfa5757d427ea6bc059df240ae829f784f Mon Sep 17 00:00:00 2001 From: Alissa Date: Fri, 4 Aug 2023 09:40:20 +0300 Subject: [PATCH 32/32] fix typo --- packages/core/src/components/DndContext/DndContext.tsx | 2 +- packages/core/src/store/actions.ts | 4 ++-- packages/core/src/store/reducer.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/components/DndContext/DndContext.tsx b/packages/core/src/components/DndContext/DndContext.tsx index 0b3885fa..d9970fae 100644 --- a/packages/core/src/components/DndContext/DndContext.tsx +++ b/packages/core/src/components/DndContext/DndContext.tsx @@ -365,7 +365,7 @@ export const DndContext = memo(function DndContext({ setStatus(Status.Initializing); activeAndOverAPI.setActive(id); dispatch({ - type: Action.SetInitiailCoordinates, + type: Action.SetInitialCoordinates, initialCoordinates, }); dispatchMonitorEvent({type: 'onDragStart', event}); diff --git a/packages/core/src/store/actions.ts b/packages/core/src/store/actions.ts index 94fb0716..5bc9ec6e 100644 --- a/packages/core/src/store/actions.ts +++ b/packages/core/src/store/actions.ts @@ -2,7 +2,7 @@ import type {Coordinates, UniqueIdentifier} from '../types'; import type {DroppableContainer} from './types'; export enum Action { - SetInitiailCoordinates = 'setInitialCoordinates', + SetInitialCoordinates = 'setInitialCoordinates', DragMove = 'dragMove', ClearCoordinates = 'clearCoordinates', DragOver = 'dragOver', @@ -13,7 +13,7 @@ export enum Action { export type Actions = | { - type: Action.SetInitiailCoordinates; + type: Action.SetInitialCoordinates; initialCoordinates: Coordinates; } | {type: Action.DragMove; coordinates: Coordinates} diff --git a/packages/core/src/store/reducer.ts b/packages/core/src/store/reducer.ts index 22b6a25f..e2f54660 100644 --- a/packages/core/src/store/reducer.ts +++ b/packages/core/src/store/reducer.ts @@ -16,7 +16,7 @@ export function getInitialState(): State { export function reducer(state: State, action: Actions): State { switch (action.type) { - case Action.SetInitiailCoordinates: + case Action.SetInitialCoordinates: return { ...state, draggable: {