From b6a9cb800dc930254b165a2ae9d1aa9883bbb34d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claude=CC=81ric=20Demers?= Date: Thu, 6 Jan 2022 22:24:46 -0500 Subject: [PATCH] Minor refactor and renaming --- .changeset/droppable-resize-observer.md | 30 ++++++++ .changeset/use-node-ref.md | 5 ++ .../src/components/DndContext/DndContext.tsx | 14 ++-- packages/core/src/hooks/useDroppable.ts | 48 +++++++----- .../hooks/utilities/useDroppableMeasuring.ts | 75 +++++++++++-------- packages/core/src/store/context.ts | 4 +- packages/core/src/store/types.ts | 4 +- .../src/components/SortableContext.tsx | 16 ++-- packages/sortable/src/hooks/useSortable.ts | 2 +- 9 files changed, 132 insertions(+), 66 deletions(-) create mode 100644 .changeset/droppable-resize-observer.md create mode 100644 .changeset/use-node-ref.md diff --git a/.changeset/droppable-resize-observer.md b/.changeset/droppable-resize-observer.md new file mode 100644 index 00000000..97169363 --- /dev/null +++ b/.changeset/droppable-resize-observer.md @@ -0,0 +1,30 @@ +--- +'@dnd-kit/core': major +'@dnd-kit/sortable': minor +--- + +Droppable containers now observe the node they are attached to via `setNodeRef` using `ResizeObserver` while dragging. + +This behaviour can be configured using the newly introduced `resizeObserverConfig` property. + +```ts +interface ResizeObserverConfig { + /** Whether the ResizeObserver should be disabled entirely */ + disabled?: boolean; + /** Resize events may affect the layout and position of other droppable containers. + * Specify an array of `UniqueIdentifier` of droppable containers that should also be re-measured + * when this droppable container resizes. Specifying an empty array re-measures all droppable containers. + */ + updateMeasurementsFor?: UniqueIdentifier[]; + /** Represents the debounce timeout between when resize events are observed and when elements are re-measured */ + timeout?: number; +} +``` + +By default, only the current droppable is re-measured when a resize event is observed. However, this may not be suitable for all use-cases. When an element resizes, it can affect the layout and position of other elements, such that it may be necessary to re-measure other droppable nodes in response to that single resize event. The `recomputeIds` property can be used to specify which droppable `id`s should be re-measured in response to resize events being observed. + +For example, the `useSortable` preset re-computes the measurements of all sortable elements after the element that resizes, so long as they are within the same `SortableContext` as the element that resizes, since it's highly likely that their layout will also shift. + +Specifying an empty array for `recomputeIds` forces all droppable containers to be re-measured. + +For consumers that were relyings on the internals of `DndContext` using `useDndContext()`, the `willRecomputeLayouts` property has been renamed to `measuringScheduled`, and the `recomputeLayouts` method has been renamed to `measureDroppableContainers`, and now optionally accepts an array of droppable `UniqueIdentifier` that should be scheduled to be re-measured. diff --git a/.changeset/use-node-ref.md b/.changeset/use-node-ref.md new file mode 100644 index 00000000..c21d2bba --- /dev/null +++ b/.changeset/use-node-ref.md @@ -0,0 +1,5 @@ +--- +'@dnd-kit/utilities': patch +--- + +The `useNodeRef` hook's `onChange` argument now receives both the current node and the previous node that were attached to the ref. diff --git a/packages/core/src/components/DndContext/DndContext.tsx b/packages/core/src/components/DndContext/DndContext.tsx index 744ed1f7..0dacd0f3 100644 --- a/packages/core/src/components/DndContext/DndContext.tsx +++ b/packages/core/src/components/DndContext/DndContext.tsx @@ -187,9 +187,9 @@ export const DndContext = memo(function DndContext({ return droppableContainers.getEnabled(); }, [droppableContainers]); const { - rectMap: droppableRects, - recomputeRects, - willRecomputeRects, + droppableRects, + measureDroppableContainers, + measuringScheduled, } = useDroppableMeasuring(enabledDroppableContainers, { dragging: isDragging, dependencies: [translate.x, translate.y], @@ -614,10 +614,10 @@ export const DndContext = memo(function DndContext({ droppableContainers, droppableRects, over, - recomputeRects, + measureDroppableContainers, scrollableAncestors, scrollableAncestorRects, - willRecomputeRects, + measuringScheduled, windowRect, }; @@ -637,10 +637,10 @@ export const DndContext = memo(function DndContext({ droppableContainers, droppableRects, over, - recomputeRects, + measureDroppableContainers, scrollableAncestors, scrollableAncestorRects, - willRecomputeRects, + measuringScheduled, windowRect, ]); diff --git a/packages/core/src/hooks/useDroppable.ts b/packages/core/src/hooks/useDroppable.ts index 4fe64e82..9441f013 100644 --- a/packages/core/src/hooks/useDroppable.ts +++ b/packages/core/src/hooks/useDroppable.ts @@ -10,9 +10,15 @@ import type {ClientRect, UniqueIdentifier} from '../types'; import {useLatestValue} from './utilities'; interface ResizeObserverConfig { + /** Whether the ResizeObserver should be disabled entirely */ disabled?: boolean; + /** Resize events may affect the layout and position of other droppable containers. + * Specify an array of `UniqueIdentifier` of droppable containers that should also be re-measured + * when this droppable container resizes. Specifying an empty array re-measures all droppable containers. + */ + updateMeasurementsFor?: UniqueIdentifier[]; + /** Represents the debounce timeout between when resize events are observed and when elements are re-measured */ timeout?: number; - recomputeIds?: UniqueIdentifier[]; } export interface UseDroppableArguments { @@ -35,28 +41,31 @@ export function useDroppable({ resizeObserverConfig, }: UseDroppableArguments) { const key = useUniqueId(ID_PREFIX); - const {active, collisions, dispatch, over, recomputeRects} = useContext( - Context - ); + const { + active, + collisions, + dispatch, + over, + measureDroppableContainers, + } = useContext(Context); + const resizeObserverConnected = useRef(false); const rect = useRef(null); - const resizeEventCount = useRef(0); const callbackId = useRef(null); const { disabled: resizeObserverDisabled, - recomputeIds, + updateMeasurementsFor, timeout: resizeObserverTimeout, } = { ...defaultResizeObserverConfig, ...resizeObserverConfig, }; - const recomputeIdsRef = useLatestValue(recomputeIds); + const ids = useLatestValue(updateMeasurementsFor ?? id); const handleResize = useCallback( () => { - const isFirstResizeEvent = resizeEventCount.current === 0; - - resizeEventCount.current++; - - if (isFirstResizeEvent) { + if (!resizeObserverConnected.current) { + // ResizeObserver invokes the `handleResize` callback as soon as `observe` is called, + // assuming the element is rendered and displayed. + resizeObserverConnected.current = true; return; } @@ -65,17 +74,21 @@ export function useDroppable({ } callbackId.current = setTimeout(() => { + measureDroppableContainers( + typeof ids.current === 'string' ? [ids.current] : ids.current + ); callbackId.current = null; - - recomputeRects(recomputeIdsRef.current ?? []); }, resizeObserverTimeout); }, //eslint-disable-next-line react-hooks/exhaustive-deps - [recomputeRects, resizeObserverTimeout] + [resizeObserverTimeout] ); const resizeObserver = useMemo( - () => (resizeObserverDisabled ? null : new ResizeObserver(handleResize)), - [handleResize, resizeObserverDisabled] + () => + !active || resizeObserverDisabled + ? null + : new ResizeObserver(handleResize), + [active, handleResize, resizeObserverDisabled] ); const handleNodeChange = useCallback( (newElement: HTMLElement | null, previousElement: HTMLElement | null) => { @@ -85,6 +98,7 @@ export function useDroppable({ if (previousElement) { resizeObserver.unobserve(previousElement); + resizeObserverConnected.current = false; } if (newElement) { diff --git a/packages/core/src/hooks/utilities/useDroppableMeasuring.ts b/packages/core/src/hooks/utilities/useDroppableMeasuring.ts index 514a1068..03b4082a 100644 --- a/packages/core/src/hooks/utilities/useDroppableMeasuring.ts +++ b/packages/core/src/hooks/utilities/useDroppableMeasuring.ts @@ -41,35 +41,40 @@ export function useDroppableMeasuring( containers: DroppableContainer[], {dragging, dependencies, config}: Arguments ) { - const [recomputeIds, setRecomputeIds] = useState( - null - ); - const willRecomputeRects = recomputeIds != null; + const [ + containerIdsScheduledForMeasurement, + setContainerIdsScheduledForMeasurement, + ] = useState(null); + const measuringScheduled = containerIdsScheduledForMeasurement != null; const {frequency, measure, strategy} = { ...defaultConfig, ...config, }; const containersRef = useRef(containers); - const recomputeRects = useCallback( + const measureDroppableContainers = useCallback( (ids: UniqueIdentifier[] = []) => - setRecomputeIds((value) => (value ? value.concat(ids) : ids)), + setContainerIdsScheduledForMeasurement((value) => + value ? value.concat(ids) : ids + ), [] ); - const recomputeRectsTimeoutId = useRef(null); + const timeoutId = useRef(null); const disabled = isDisabled(); - const rectMap = useLazyMemo( + const droppableRects = useLazyMemo( (previousValue) => { if (disabled && !dragging) { return defaultValue; } + const ids = containerIdsScheduledForMeasurement; + if ( !previousValue || previousValue === defaultValue || containersRef.current !== containers || - recomputeIds != null + ids != null ) { - const rectMap: RectMap = new Map(); + const map: RectMap = new Map(); for (let container of containers) { if (!container) { @@ -77,13 +82,13 @@ export function useDroppableMeasuring( } if ( - recomputeIds && - recomputeIds.length > 0 && - !recomputeIds.includes(container.id) && + ids && + ids.length > 0 && + !ids.includes(container.id) && container.rect.current ) { - // This container does not need to be recomputed - rectMap.set(container.id, container.rect.current); + // This container does not need to be re-measured + map.set(container.id, container.rect.current); continue; } @@ -93,16 +98,22 @@ export function useDroppableMeasuring( container.rect.current = rect; if (rect) { - rectMap.set(container.id, rect); + map.set(container.id, rect); } } - return rectMap; + return map; } return previousValue; }, - [containers, dragging, disabled, measure, recomputeIds] + [ + containers, + containerIdsScheduledForMeasurement, + dragging, + disabled, + measure, + ] ); useEffect(() => { @@ -110,46 +121,46 @@ export function useDroppableMeasuring( }, [containers]); useEffect( - function recompute() { + () => { if (disabled) { return; } - requestAnimationFrame(() => recomputeRects()); + requestAnimationFrame(() => measureDroppableContainers()); }, // eslint-disable-next-line react-hooks/exhaustive-deps [dragging, disabled] ); useEffect(() => { - if (willRecomputeRects) { - setRecomputeIds(null); + if (measuringScheduled) { + setContainerIdsScheduledForMeasurement(null); } - }, [willRecomputeRects]); + }, [measuringScheduled]); useEffect( - function forceRecomputeLayouts() { + () => { if ( disabled || typeof frequency !== 'number' || - recomputeRectsTimeoutId.current !== null + timeoutId.current !== null ) { return; } - recomputeRectsTimeoutId.current = setTimeout(() => { - recomputeRects(); - recomputeRectsTimeoutId.current = null; + timeoutId.current = setTimeout(() => { + measureDroppableContainers(); + timeoutId.current = null; }, frequency); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [frequency, disabled, recomputeRects, ...dependencies] + [frequency, disabled, measureDroppableContainers, ...dependencies] ); return { - rectMap, - recomputeRects, - willRecomputeRects, + droppableRects, + measureDroppableContainers, + measuringScheduled, }; function isDisabled() { diff --git a/packages/core/src/store/context.ts b/packages/core/src/store/context.ts index ed57013d..6199a7df 100644 --- a/packages/core/src/store/context.ts +++ b/packages/core/src/store/context.ts @@ -29,7 +29,7 @@ export const Context = createContext({ }, scrollableAncestors: [], scrollableAncestorRects: [], - recomputeRects: noop, + measureDroppableContainers: noop, windowRect: null, - willRecomputeRects: false, + measuringScheduled: false, }); diff --git a/packages/core/src/store/types.ts b/packages/core/src/store/types.ts index 0ffc9a5d..6912f600 100644 --- a/packages/core/src/store/types.ts +++ b/packages/core/src/store/types.ts @@ -94,7 +94,7 @@ export interface DndContextDescriptor { }; scrollableAncestors: Element[]; scrollableAncestorRects: ClientRect[]; - recomputeRects(ids: UniqueIdentifier[]): void; - willRecomputeRects: boolean; + measureDroppableContainers(ids: UniqueIdentifier[]): void; + measuringScheduled: boolean; windowRect: ClientRect | null; } diff --git a/packages/sortable/src/components/SortableContext.tsx b/packages/sortable/src/components/SortableContext.tsx index 839b78d9..3a428462 100644 --- a/packages/sortable/src/components/SortableContext.tsx +++ b/packages/sortable/src/components/SortableContext.tsx @@ -50,8 +50,8 @@ export function SortableContext({ dragOverlay, droppableRects, over, - recomputeRects, - willRecomputeRects, + measureDroppableContainers, + measuringScheduled, } = useDndContext(); const containerId = useUniqueId(ID_PREFIX, id); const useDragOverlay = Boolean(dragOverlay.rect !== null); @@ -73,10 +73,16 @@ export function SortableContext({ (overIndex !== -1 && activeIndex === -1) || itemsHaveChanged; useIsomorphicLayoutEffect(() => { - if (itemsHaveChanged && isDragging && !willRecomputeRects) { - recomputeRects(items); + if (itemsHaveChanged && isDragging && !measuringScheduled) { + measureDroppableContainers(items); } - }, [itemsHaveChanged, items, isDragging, recomputeRects, willRecomputeRects]); + }, [ + itemsHaveChanged, + items, + isDragging, + measureDroppableContainers, + measuringScheduled, + ]); useEffect(() => { previousItemsRef.current = items; diff --git a/packages/sortable/src/hooks/useSortable.ts b/packages/sortable/src/hooks/useSortable.ts index bf7c2bfe..9fb278ed 100644 --- a/packages/sortable/src/hooks/useSortable.ts +++ b/packages/sortable/src/hooks/useSortable.ts @@ -75,7 +75,7 @@ export function useSortable({ id, data, resizeObserverConfig: { - recomputeIds: itemsAfterCurrentSortable, + updateMeasurementsFor: itemsAfterCurrentSortable, ...resizeObserverConfig, }, });