Skip to content

Commit

Permalink
Fallback to initial rect when active node unmounts during initialization
Browse files Browse the repository at this point in the history
  • Loading branch information
clauderic committed May 17, 2022
1 parent 40707ce commit 30efac9
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 41 deletions.
5 changes: 5 additions & 0 deletions .changeset/active-rect-fallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@dnd-kit/core': patch
---

Fallback to initial rect measured for the active draggable node if it unmounts during initialization (after `onDragStart` is dispatched).
31 changes: 16 additions & 15 deletions packages/core/src/components/DndContext/DndContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ import {
useSensorSetup,
useRects,
useWindowRect,
useInitialValue,
useRect,
useRectDelta,
useScrollOffsets,
useScrollOffsetsDelta,
} from '../../hooks/utilities';
Expand All @@ -57,7 +57,6 @@ import {
CollisionDetection,
defaultCoordinates,
getAdjustedRect,
getRectDelta,
getFirstCollision,
rectIntersection,
} from '../../utilities';
Expand Down Expand Up @@ -196,16 +195,25 @@ export const DndContext = memo(function DndContext({
const layoutShiftCompensationDisabled =
activationCoordinates == null ||
autoScrollOptions.layoutShiftCompensation === false;
const setInitialRect = useLayoutShiftScrollCompensation(activeNode, {
const initialActiveNodeRect = useMemo(
() =>
activeNode ? measuringConfiguration.draggable.measure(activeNode) : null,
// eslint-disable-next-line react-hooks/exhaustive-deps
[activeNode, measuringConfiguration.draggable.measure]
);

useLayoutShiftScrollCompensation({
disabled: layoutShiftCompensationDisabled,
element: activeNode,
initialRect: initialActiveNodeRect,
measure: measuringConfiguration.draggable.measure,
});

const activeNodeRect = useRect(
activeNode,
measuringConfiguration.draggable.measure
measuringConfiguration.draggable.measure,
initialActiveNodeRect
);
const initialActiveNodeRect = useInitialValue(activeNodeRect);
const containerNodeRect = useRect(
activeNode ? activeNode.parentElement : null
);
Expand Down Expand Up @@ -238,11 +246,9 @@ export const DndContext = memo(function DndContext({
const usesDragOverlay = Boolean(
dragOverlay.nodeRef.current && dragOverlay.rect
);
const nodeRectDelta = usesDragOverlay
? defaultCoordinates
: // The delta between the previous and new position of the draggable node
// is only relevant when there is no drag overlay
getRectDelta(activeNodeRect, initialActiveNodeRect);
// The delta between the previous and new position of the draggable node
// is only relevant when there is no drag overlay
const nodeRectDelta = useRectDelta(usesDragOverlay ? null : activeNodeRect);

// Get the window rect of the dragging node
const windowRect = useWindowRect(
Expand Down Expand Up @@ -358,11 +364,6 @@ export const DndContext = memo(function DndContext({
const event: DragStartEvent = {
active: {id, data: draggableNode.data, rect: activeRects},
};
const node = draggableNode.node.current;

if (node) {
setInitialRect(node);
}

unstable_batchedUpdates(() => {
onDragStart?.(event);
Expand Down
46 changes: 21 additions & 25 deletions packages/core/src/components/DndContext/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import {useCallback, useMemo, useRef} from 'react';
import {useMemo, useRef} from 'react';
import {useIsomorphicLayoutEffect} from '@dnd-kit/utilities';
import type {DeepRequired} from '@dnd-kit/utilities';

import {getRectDelta} from '../../utilities/rect';
import {getFirstScrollableAncestor} from '../../utilities/scroll';
import {useInitialValue} from '../../hooks/utilities';
import type {ClientRect} from '../../types';
import {defaultMeasuringConfiguration} from './defaults';
import type {MeasuringFunction, MeasuringConfiguration} from './types';
Expand Down Expand Up @@ -33,32 +32,38 @@ export function useMeasuringConfiguration(
}

interface Options {
disabled?: boolean;
disabled: boolean;
element: HTMLElement | null;
initialRect: ClientRect | null;
measure: MeasuringFunction;
}

export function useLayoutShiftScrollCompensation(
node: HTMLElement | null,
{disabled, measure}: Options
) {
const initialNode = useInitialValue(node);
const initialRect = useRef<ClientRect | null>(null);
export function useLayoutShiftScrollCompensation({
element,
measure,
initialRect,
disabled,
}: Options) {
const initialized = useRef(false);

useIsomorphicLayoutEffect(() => {
if (!initialNode) {
initialRect.current = null;
if (disabled) {
initialized.current = false;
return;
}

if (disabled || !initialRect.current) {
if (!initialRect || !element || initialized.current) {
return;
}

const rect = measure(initialNode);
const rectDelta = getRectDelta(rect, initialRect.current);
const rect = measure(element);
const rectDelta = getRectDelta(rect, initialRect);

// Only perform layout shift scroll compensation once
initialized.current = true;

if (rectDelta.x > 0 || rectDelta.y > 0) {
const firstScrollableAncestor = getFirstScrollableAncestor(initialNode);
const firstScrollableAncestor = getFirstScrollableAncestor(element);

if (firstScrollableAncestor) {
firstScrollableAncestor.scrollBy({
Expand All @@ -67,14 +72,5 @@ export function useLayoutShiftScrollCompensation(
});
}
}
}, [disabled, initialNode, measure]);

const measureRect = useCallback(
(node: HTMLElement) => {
initialRect.current = measure(node);
},
[measure]
);

return measureRect;
}, [disabled, initialRect, measure, element]);
}
1 change: 1 addition & 0 deletions packages/core/src/hooks/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export {
export type {DroppableMeasuring} from './useDroppableMeasuring';
export {useInitialValue} from './useInitialValue';
export {useRect} from './useRect';
export {useRectDelta} from './useRectDelta';
export {useResizeObserver} from './useResizeObserver';
export {useScrollableAncestors} from './useScrollableAncestors';
export {useScrollIntoViewIfNeeded} from './useScrollIntoViewIfNeeded';
Expand Down
10 changes: 9 additions & 1 deletion packages/core/src/hooks/utilities/useRect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import {useResizeObserver} from './useResizeObserver';

export function useRect(
element: HTMLElement | null,
measure: (element: HTMLElement) => ClientRect = getClientRect
measure: (element: HTMLElement) => ClientRect = getClientRect,
fallbackRect?: ClientRect | null
) {
const [rect, measureRect] = useReducer(reducer, null);

const mutationObserver = useMutationObserver({
callback(records) {
if (!element) {
Expand Down Expand Up @@ -56,6 +58,12 @@ export function useRect(
return null;
}

if (element.isConnected === false) {
// Fall back to last rect we measured if the element is
// no longer connected to the DOM.
return currentRect ?? fallbackRect ?? null;
}

const newRect = measure(element);

if (JSON.stringify(currentRect) === JSON.stringify(newRect)) {
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/hooks/utilities/useRectDelta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type {ClientRect} from '../../types';
import {getRectDelta} from '../../utilities';

import {useInitialValue} from './useInitialValue';

export function useRectDelta(rect: ClientRect | null) {
const initialRect = useInitialValue(rect);

return getRectDelta(rect, initialRect);
}

0 comments on commit 30efac9

Please sign in to comment.