Skip to content

Commit

Permalink
Minor refactor and renaming
Browse files Browse the repository at this point in the history
  • Loading branch information
clauderic committed Jan 7, 2022
1 parent 823d4d2 commit f9d9a2f
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 63 deletions.
31 changes: 31 additions & 0 deletions .changeset/droppable-resize-observer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
'@dnd-kit/core': major
'@dnd-kit/sortable': minor
'@dnd-kit/utilities': patch
---

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.
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,
recomputeRects,
willRecomputeRects,
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,
recomputeRects,
measureDroppableContainers,
scrollableAncestors,
scrollableAncestorRects,
willRecomputeRects,
measuringScheduled,
windowRect,
};

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

Expand Down
48 changes: 31 additions & 17 deletions packages/core/src/hooks/useDroppable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<ClientRect | null>(null);
const resizeEventCount = useRef(0);
const callbackId = useRef<NodeJS.Timeout | null>(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;
}

Expand All @@ -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) => {
Expand All @@ -85,6 +98,7 @@ export function useDroppable({

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

if (newElement) {
Expand Down
69 changes: 40 additions & 29 deletions packages/core/src/hooks/utilities/useDroppableMeasuring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,49 +41,54 @@ export function useDroppableMeasuring(
containers: DroppableContainer[],
{dragging, dependencies, config}: Arguments
) {
const [recomputeIds, setRecomputeIds] = useState<UniqueIdentifier[] | null>(
null
);
const willRecomputeRects = recomputeIds != null;
const [
containerIdsScheduledForMeasurement,
setContainerIdsScheduledForMeasurement,
] = useState<UniqueIdentifier[] | null>(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<NodeJS.Timeout | null>(null);
const timeoutId = useRef<NodeJS.Timeout | null>(null);
const disabled = isDisabled();
const rectMap = useLazyMemo<RectMap>(
const droppableRects = useLazyMemo<RectMap>(
(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) {
continue;
}

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);
map.set(container.id, container.rect.current);
continue;
}

Expand All @@ -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(() => {
Expand All @@ -115,41 +126,41 @@ export function useDroppableMeasuring(
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() {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/store/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const Context = createContext<DndContextDescriptor>({
},
scrollableAncestors: [],
scrollableAncestorRects: [],
recomputeRects: noop,
measureDroppableContainers: noop,
windowRect: null,
willRecomputeRects: false,
measuringScheduled: false,
});
4 changes: 2 additions & 2 deletions packages/core/src/store/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
16 changes: 11 additions & 5 deletions packages/sortable/src/components/SortableContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/sortable/src/hooks/useSortable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function useSortable({
id,
data,
resizeObserverConfig: {
recomputeIds: itemsAfterCurrentSortable,
updateMeasurementsFor: itemsAfterCurrentSortable,
...resizeObserverConfig,
},
});
Expand Down

0 comments on commit f9d9a2f

Please sign in to comment.