From c50f0a90d8493255e068c36b5c906f786c3e9c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claude=CC=81ric=20Demers?= Date: Thu, 6 Jan 2022 17:54:59 -0500 Subject: [PATCH] Add resize observer to droppable elements while dragging While sorting, it's possible for the size of droppable elements to change. We add a resize observer to keep track of size changes and schedule recomputation of the bounding rectangles of the droppable elements when resize events occur. The `resizeObserverConfig` param can be used to disable or configure the resize observer. --- packages/core/src/hooks/useDraggable.ts | 4 +- packages/core/src/hooks/useDroppable.ts | 80 +++++++++++++++++-- packages/core/src/hooks/utilities/index.ts | 2 +- .../{useData.ts => useLatestValue.ts} | 6 +- packages/sortable/src/hooks/useSortable.ts | 20 ++++- packages/utilities/src/hooks/useNodeRef.ts | 9 ++- 6 files changed, 103 insertions(+), 18 deletions(-) rename packages/core/src/hooks/utilities/{useData.ts => useLatestValue.ts} (66%) diff --git a/packages/core/src/hooks/useDraggable.ts b/packages/core/src/hooks/useDraggable.ts index 7efab94ae..cf38d4db3 100644 --- a/packages/core/src/hooks/useDraggable.ts +++ b/packages/core/src/hooks/useDraggable.ts @@ -9,7 +9,7 @@ import { import {Context, Data} from '../store'; import {ActiveDraggableContext} from '../components/DndContext'; import { - useData, + useLatestValue, useSyntheticListeners, SyntheticListenerMap, } from './utilities'; @@ -58,7 +58,7 @@ export function useDraggable({ ); const [node, setNodeRef] = useNodeRef(); const listeners = useSyntheticListeners(activators, id); - const dataRef = useData(data); + const dataRef = useLatestValue(data); useIsomorphicLayoutEffect( () => { diff --git a/packages/core/src/hooks/useDroppable.ts b/packages/core/src/hooks/useDroppable.ts index db4f3961e..26905b1b0 100644 --- a/packages/core/src/hooks/useDroppable.ts +++ b/packages/core/src/hooks/useDroppable.ts @@ -1,4 +1,4 @@ -import {useContext, useEffect, useRef} from 'react'; +import {useCallback, useContext, useEffect, useMemo, useRef} from 'react'; import { useIsomorphicLayoutEffect, useNodeRef, @@ -6,27 +6,93 @@ import { } from '@dnd-kit/utilities'; import {Context, Action, Data} from '../store'; -import type {ClientRect} from '../types'; -import {useData} from './utilities'; +import type {ClientRect, UniqueIdentifier} from '../types'; +import {useLatestValue} from './utilities'; + +interface ResizeObserverConfig { + disabled?: boolean; + timeout?: number; + recomputeIds?: UniqueIdentifier[]; +} export interface UseDroppableArguments { - id: string; + id: UniqueIdentifier; disabled?: boolean; data?: Data; + resizeObserverConfig?: ResizeObserverConfig; } const ID_PREFIX = 'Droppable'; +const defaultResizeObserverConfig = { + timeout: 50, +}; + export function useDroppable({ data, disabled = false, id, + resizeObserverConfig, }: UseDroppableArguments) { const key = useUniqueId(ID_PREFIX); - const {active, dispatch, over} = useContext(Context); + const {active, dispatch, over, recomputeRects} = useContext(Context); const rect = useRef(null); - const [nodeRef, setNodeRef] = useNodeRef(); - const dataRef = useData(data); + const resizeEventCount = useRef(0); + const callbackId = useRef(null); + const { + disabled: resizeObserverDisabled, + recomputeIds, + timeout: resizeObserverTimeout, + } = { + ...defaultResizeObserverConfig, + ...resizeObserverConfig, + }; + const recomputeIdsRef = useLatestValue(recomputeIds); + const handleResize = useCallback( + () => { + const isFirstResizeEvent = resizeEventCount.current === 0; + + resizeEventCount.current++; + + if (isFirstResizeEvent) { + return; + } + + if (callbackId.current != null) { + clearTimeout(callbackId.current); + } + + callbackId.current = setTimeout(() => { + callbackId.current = null; + + recomputeRects(recomputeIdsRef.current ?? []); + }, resizeObserverTimeout); + }, + //eslint-disable-next-line react-hooks/exhaustive-deps + [recomputeRects, resizeObserverTimeout] + ); + const resizeObserver = useMemo( + () => (resizeObserverDisabled ? null : new ResizeObserver(handleResize)), + [handleResize, resizeObserverDisabled] + ); + const handleNodeChange = useCallback( + (newElement: HTMLElement | null, previousElement: HTMLElement | null) => { + if (!resizeObserver) { + return; + } + + if (previousElement) { + resizeObserver.unobserve(previousElement); + } + + if (newElement) { + resizeObserver.observe(newElement); + } + }, + [resizeObserver] + ); + const [nodeRef, setNodeRef] = useNodeRef(handleNodeChange); + const dataRef = useLatestValue(data); useIsomorphicLayoutEffect( () => { diff --git a/packages/core/src/hooks/utilities/index.ts b/packages/core/src/hooks/utilities/index.ts index 64b88f358..a187ed21c 100644 --- a/packages/core/src/hooks/utilities/index.ts +++ b/packages/core/src/hooks/utilities/index.ts @@ -6,7 +6,7 @@ export { export type {Options as AutoScrollOptions} from './useAutoScroller'; export {useCachedNode} from './useCachedNode'; export {useCombineActivators} from './useCombineActivators'; -export {useData} from './useData'; +export {useLatestValue} from './useLatestValue'; export { useDroppableMeasuring, MeasuringFrequency, diff --git a/packages/core/src/hooks/utilities/useData.ts b/packages/core/src/hooks/utilities/useLatestValue.ts similarity index 66% rename from packages/core/src/hooks/utilities/useData.ts rename to packages/core/src/hooks/utilities/useLatestValue.ts index 0aaba4f30..9e32185b7 100644 --- a/packages/core/src/hooks/utilities/useData.ts +++ b/packages/core/src/hooks/utilities/useLatestValue.ts @@ -1,10 +1,8 @@ import {useRef} from 'react'; import {useIsomorphicLayoutEffect} from '@dnd-kit/utilities'; -import type {Data} from '../../store'; - -export function useData(data: Data | undefined) { - const dataRef = useRef(data); +export function useLatestValue(data: T) { + const dataRef = useRef(data); useIsomorphicLayoutEffect(() => { if (dataRef.current !== data) { diff --git a/packages/sortable/src/hooks/useSortable.ts b/packages/sortable/src/hooks/useSortable.ts index 934183575..30c90c5ab 100644 --- a/packages/sortable/src/hooks/useSortable.ts +++ b/packages/sortable/src/hooks/useSortable.ts @@ -1,5 +1,10 @@ import {useContext, useEffect, useMemo, useRef} from 'react'; -import {useDraggable, useDroppable, UseDraggableArguments} from '@dnd-kit/core'; +import { + useDraggable, + useDroppable, + UseDraggableArguments, + UseDroppableArguments, +} from '@dnd-kit/core'; import {CSS, useCombinedRefs} from '@dnd-kit/utilities'; import {Context} from '../components'; @@ -20,7 +25,9 @@ import type { } from './types'; import {useDerivedTransform} from './utilities'; -export interface Arguments extends UseDraggableArguments { +export interface Arguments + extends UseDraggableArguments, + Pick { animateLayoutChanges?: AnimateLayoutChanges; getNewIndex?: NewIndexGetter; strategy?: SortingStrategy; @@ -35,6 +42,7 @@ export function useSortable({ getNewIndex = defaultNewIndexGetter, id, strategy: localStrategy, + resizeObserverConfig, transition = defaultTransition, }: Arguments) { const { @@ -53,9 +61,17 @@ export function useSortable({ () => ({sortable: {containerId, index, items}, ...customData}), [containerId, customData, index, items] ); + const itemsAfterCurrentSortable = useMemo( + () => items.slice(items.indexOf(id)), + [items, id] + ); const {rect, node, setNodeRef: setDroppableNodeRef} = useDroppable({ id, data, + resizeObserverConfig: { + recomputeIds: itemsAfterCurrentSortable, + ...resizeObserverConfig, + }, }); const { active, diff --git a/packages/utilities/src/hooks/useNodeRef.ts b/packages/utilities/src/hooks/useNodeRef.ts index 2f6f04618..060c7058a 100644 --- a/packages/utilities/src/hooks/useNodeRef.ts +++ b/packages/utilities/src/hooks/useNodeRef.ts @@ -1,11 +1,16 @@ import {useRef, useCallback} from 'react'; -export function useNodeRef(onChange?: (element: HTMLElement | null) => void) { +export function useNodeRef( + onChange?: ( + newElement: HTMLElement | null, + previousElement: HTMLElement | null + ) => void +) { const node = useRef(null); const setNodeRef = useCallback( (element: HTMLElement | null) => { + onChange?.(element, node.current); node.current = element; - onChange?.(element); }, [onChange] );