Skip to content

Commit

Permalink
Add resize observer to droppable elements while dragging
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
clauderic committed Jan 6, 2022
1 parent dfa11b0 commit c50f0a9
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 18 deletions.
4 changes: 2 additions & 2 deletions packages/core/src/hooks/useDraggable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import {Context, Data} from '../store';
import {ActiveDraggableContext} from '../components/DndContext';
import {
useData,
useLatestValue,
useSyntheticListeners,
SyntheticListenerMap,
} from './utilities';
Expand Down Expand Up @@ -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(
() => {
Expand Down
80 changes: 73 additions & 7 deletions packages/core/src/hooks/useDroppable.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,98 @@
import {useContext, useEffect, useRef} from 'react';
import {useCallback, useContext, useEffect, useMemo, useRef} from 'react';
import {
useIsomorphicLayoutEffect,
useNodeRef,
useUniqueId,
} 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<ClientRect | null>(null);
const [nodeRef, setNodeRef] = useNodeRef();
const dataRef = useData(data);
const resizeEventCount = useRef(0);
const callbackId = useRef<NodeJS.Timeout | null>(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(
() => {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/hooks/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<T extends any>(data: T) {
const dataRef = useRef<T>(data);

useIsomorphicLayoutEffect(() => {
if (dataRef.current !== data) {
Expand Down
20 changes: 18 additions & 2 deletions packages/sortable/src/hooks/useSortable.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -20,7 +25,9 @@ import type {
} from './types';
import {useDerivedTransform} from './utilities';

export interface Arguments extends UseDraggableArguments {
export interface Arguments
extends UseDraggableArguments,
Pick<UseDroppableArguments, 'resizeObserverConfig'> {
animateLayoutChanges?: AnimateLayoutChanges;
getNewIndex?: NewIndexGetter;
strategy?: SortingStrategy;
Expand All @@ -35,6 +42,7 @@ export function useSortable({
getNewIndex = defaultNewIndexGetter,
id,
strategy: localStrategy,
resizeObserverConfig,
transition = defaultTransition,
}: Arguments) {
const {
Expand All @@ -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,
Expand Down
9 changes: 7 additions & 2 deletions packages/utilities/src/hooks/useNodeRef.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLElement | null>(null);
const setNodeRef = useCallback(
(element: HTMLElement | null) => {
onChange?.(element, node.current);
node.current = element;
onChange?.(element);
},
[onChange]
);
Expand Down

0 comments on commit c50f0a9

Please sign in to comment.