Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Observe resizes of droppable containers while dragging #561

Merged
merged 4 commits into from
Jan 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .changeset/droppable-resize-observer.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions .changeset/use-node-ref.md
Original file line number Diff line number Diff line change
@@ -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.
14 changes: 7 additions & 7 deletions packages/core/src/components/DndContext/DndContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,9 @@ export const DndContext = memo(function DndContext({
return droppableContainers.getEnabled();
}, [droppableContainers]);
const {
rectMap: droppableRects,
recomputeLayouts,
willRecomputeLayouts,
droppableRects,
measureDroppableContainers,
measuringScheduled,
} = useDroppableMeasuring(enabledDroppableContainers, {
dragging: isDragging,
dependencies: [translate.x, translate.y],
Expand Down Expand Up @@ -614,10 +614,10 @@ export const DndContext = memo(function DndContext({
droppableContainers,
droppableRects,
over,
recomputeLayouts,
measureDroppableContainers,
scrollableAncestors,
scrollableAncestorRects,
willRecomputeLayouts,
measuringScheduled,
windowRect,
};

Expand All @@ -637,10 +637,10 @@ export const DndContext = memo(function DndContext({
droppableContainers,
droppableRects,
over,
recomputeLayouts,
measureDroppableContainers,
scrollableAncestors,
scrollableAncestorRects,
willRecomputeLayouts,
measuringScheduled,
windowRect,
]);

Expand Down
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
96 changes: 89 additions & 7 deletions packages/core/src/hooks/useDroppable.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,114 @@
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 {
/** 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;
}

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, collisions, dispatch, over} = useContext(Context);
const {
active,
collisions,
dispatch,
over,
measureDroppableContainers,
} = useContext(Context);
const resizeObserverConnected = useRef(false);
const rect = useRef<ClientRect | null>(null);
const [nodeRef, setNodeRef] = useNodeRef();
const dataRef = useData(data);
const callbackId = useRef<NodeJS.Timeout | null>(null);
const {
disabled: resizeObserverDisabled,
updateMeasurementsFor,
timeout: resizeObserverTimeout,
} = {
...defaultResizeObserverConfig,
...resizeObserverConfig,
};
const ids = useLatestValue(updateMeasurementsFor ?? id);
const handleResize = useCallback(
() => {
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;
}

if (callbackId.current != null) {
clearTimeout(callbackId.current);
}

callbackId.current = setTimeout(() => {
measureDroppableContainers(
typeof ids.current === 'string' ? [ids.current] : ids.current
);
callbackId.current = null;
}, resizeObserverTimeout);
},
//eslint-disable-next-line react-hooks/exhaustive-deps
[resizeObserverTimeout]
);
const resizeObserver = useMemo(
() =>
!active || resizeObserverDisabled
? null
: new ResizeObserver(handleResize),
[active, handleResize, resizeObserverDisabled]
);
const handleNodeChange = useCallback(
(newElement: HTMLElement | null, previousElement: HTMLElement | null) => {
if (!resizeObserver) {
return;
}

if (previousElement) {
resizeObserver.unobserve(previousElement);
resizeObserverConnected.current = false;
}

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
Loading