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

Allow consumers to determine when to perform layout animations and when to measure nodes #144

Merged
merged 12 commits into from
Mar 24, 2021
6 changes: 6 additions & 0 deletions .changeset/layout-measuring-animation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@dnd-kit/core': minor
'@dnd-kit/sortable': minor
---

Allow consumers to determine whether to animate layout changes and when to measure nodes. Consumers can now use the `animateLayoutChanges` prop of `useSortable` to determine whether layout animations should occur. Consumers can now also decide when to measure layouts, and at what frequency using the `layoutMeasuring` prop of `DndContext`. By default, `DndContext` will measure layouts just-in-time after sorting has begun. Consumers can override this behaviour to either only measure before dragging begins (on mount and after dragging), or always (on mount, before dragging, after dragging). Pairing the `layoutMeasuring` prop on `DndContext` and the `animateLayoutChanges` prop of `useSortable` opens up a number of new possibilities for consumers, such as animating insertion and removal of items in a sortable list.
28 changes: 13 additions & 15 deletions packages/core/src/components/DndContext/DndContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,17 @@ import {
} from '../../store';
import type {Coordinates, ViewRect, LayoutRect, Translate} from '../../types';
import {
LayoutMeasuring,
SyntheticListener,
useAutoScroller,
useCachedNode,
useCombineActivators,
useLayoutRectMap,
useLayoutMeasuring,
useScrollableAncestors,
useClientRect,
useClientRects,
useScrollOffsets,
useViewRect,
SyntheticListener,
} from '../../hooks/utilities';
import {
KeyboardSensor,
Expand Down Expand Up @@ -125,9 +126,10 @@ interface Props {
cancelDrop?: CancelDrop;
children?: React.ReactNode;
collisionDetection?: CollisionDetection;
layoutMeasuring?: Partial<LayoutMeasuring>;
modifiers?: Modifiers;
screenReaderInstructions?: ScreenReaderInstructions;
sensors?: SensorDescriptor<any>[];
modifiers?: Modifiers;
onDragStart?(event: DragStartEvent): void;
onDragMove?(event: DragMoveEvent): void;
onDragOver?(event: DragOverEvent): void;
Expand All @@ -152,8 +154,9 @@ export const DndContext = memo(function DndContext({
children,
sensors = defaultSensors,
collisionDetection = rectIntersection,
screenReaderInstructions = defaultScreenReaderInstructions,
layoutMeasuring,
modifiers,
screenReaderInstructions = defaultScreenReaderInstructions,
...props
}: Props) {
const store = useReducer(reducer, undefined, getInitialState);
Expand All @@ -167,12 +170,15 @@ export const DndContext = memo(function DndContext({
const [activatorEvent, setActivatorEvent] = useState<Event | null>(null);
const latestProps = useRef(props);
const draggableDescribedById = useUniqueId(`DndDescribedBy`);

const {
layoutRectMap: droppableRects,
recomputeLayouts,
willRecomputeLayouts,
} = useLayoutRectMap(droppableContainers, active === null);
} = useLayoutMeasuring(droppableContainers, {
dragging: active != null,
dependencies: [translate.x, translate.y],
config: layoutMeasuring,
});
const activeNode = useCachedNode(
getDraggableNode(active, draggableNodes),
active
Expand Down Expand Up @@ -421,15 +427,6 @@ export const DndContext = memo(function DndContext({
Object.values(props)
);

useIsomorphicLayoutEffect(() => {
if (!active) {
return;
}

// Recompute rects right after dragging has begun in case they have changed
recomputeLayouts();
}, [active, recomputeLayouts]);

useEffect(() => {
if (!active) {
initialActiveNodeRectRef.current = null;
Expand All @@ -453,6 +450,7 @@ export const DndContext = memo(function DndContext({
if (!onDragMove || !translatedRect) {
return;
}

const overNodeRect = getLayoutRect(overId, droppableRects);

onDragMove({
Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/components/DndContext/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export {
ActiveDraggableContext,
export {ActiveDraggableContext, DndContext} from './DndContext';
export type {
CancelDrop,
DndContext,
DragStartEvent,
DragMoveEvent,
DragOverEvent,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ export {
defaultAnnouncements,
ScreenReaderInstructions,
} from './Accessibility';
export {
export {DndContext} from './DndContext';
export type {
CancelDrop,
DndContext,
DragEndEvent,
DragOverEvent,
DragMoveEvent,
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export {
} from './useDraggable';
export {useDndContext, UseDndContextReturnValue} from './useDndContext';
export {useDroppable, UseDroppableArguments} from './useDroppable';
export {LayoutMeasuringStrategy, LayoutMeasuringFrequency} from './utilities';
export type {LayoutMeasuring} from './utilities';
7 changes: 6 additions & 1 deletion packages/core/src/hooks/utilities/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
export {useAutoScroller} from './useAutoScroller';
export {useCachedNode} from './useCachedNode';
export {useCombineActivators} from './useCombineActivators';
export {useLayoutRectMap} from './useLayoutRectMap';
export {
useLayoutMeasuring,
LayoutMeasuringFrequency,
LayoutMeasuringStrategy,
} from './useLayoutMeasuring';
export type {LayoutMeasuring} from './useLayoutMeasuring';
export {useScrollOffsets} from './useScrollOffsets';
export {useScrollableAncestors} from './useScrollableAncestors';
export {
Expand Down
167 changes: 167 additions & 0 deletions packages/core/src/hooks/utilities/useLayoutMeasuring.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import {useCallback, useEffect, useRef, useState} from 'react';
import {useLazyMemo} from '@dnd-kit/utilities';

import {getElementLayout} from '../../utilities';
import type {DroppableContainers, LayoutRectMap} from '../../store/types';

interface Arguments {
dragging: boolean;
dependencies: any[];
config: Partial<LayoutMeasuring> | undefined;
}

export enum LayoutMeasuringStrategy {
Always,
BeforeDragging,
WhileDragging,
}

export enum LayoutMeasuringFrequency {
Optimized = 'optimized',
}

export interface LayoutMeasuring {
strategy: LayoutMeasuringStrategy;
frequency: LayoutMeasuringFrequency | number;
}

const defaultValue: LayoutRectMap = new Map();

export function useLayoutMeasuring(
containers: DroppableContainers,
{dragging, dependencies, config}: Arguments
) {
const [willRecomputeLayouts, setWillRecomputeLayouts] = useState(false);
const {frequency, strategy} = getLayoutMeasuring(config);
const containersRef = useRef(containers);
const recomputeLayouts = useCallback(() => setWillRecomputeLayouts(true), []);
const recomputeLayoutsTimeoutId = useRef<NodeJS.Timeout | null>(null);
const disabled = isDisabled();
const layoutRectMap = useLazyMemo<LayoutRectMap>(
(previousValue) => {
if (disabled && !dragging) {
return defaultValue;
}

if (
!previousValue ||
previousValue === defaultValue ||
containersRef.current !== containers ||
willRecomputeLayouts
) {
for (let container of Object.values(containers)) {
if (!container) {
continue;
}

container.rect.current = container.node.current
? getElementLayout(container.node.current)
: null;
}

return createLayoutRectMap(containers);
}

return previousValue;
},
[containers, dragging, disabled, willRecomputeLayouts]
);

useEffect(() => {
containersRef.current = containers;
}, [containers]);

useEffect(() => {
if (willRecomputeLayouts) {
setWillRecomputeLayouts(false);
}
}, [willRecomputeLayouts]);

useEffect(
function recompute() {
if (disabled) {
return;
}

requestAnimationFrame(recomputeLayouts);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[dragging, disabled]
);

useEffect(
function forceRecomputeLayouts() {
if (
disabled ||
typeof frequency !== 'number' ||
recomputeLayoutsTimeoutId.current !== null
) {
return;
}

recomputeLayoutsTimeoutId.current = setTimeout(() => {
recomputeLayouts();
recomputeLayoutsTimeoutId.current = null;
}, frequency);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[frequency, disabled, recomputeLayouts, ...dependencies]
);

return {
layoutRectMap,
recomputeLayouts,
willRecomputeLayouts,
};

function isDisabled() {
switch (strategy) {
case LayoutMeasuringStrategy.Always:
return false;
case LayoutMeasuringStrategy.BeforeDragging:
return dragging;
default:
return !dragging;
}
}
}

function createLayoutRectMap(
containers: DroppableContainers | null
): LayoutRectMap {
const layoutRectMap: LayoutRectMap = new Map();

if (containers) {
for (const container of Object.values(containers)) {
if (!container) {
continue;
}

const {id, rect, disabled} = container;

if (disabled || rect.current == null) {
continue;
}

layoutRectMap.set(id, rect.current);
}
}

return layoutRectMap;
}

const defaultLayoutMeasuring: LayoutMeasuring = {
strategy: LayoutMeasuringStrategy.WhileDragging,
frequency: LayoutMeasuringFrequency.Optimized,
};

function getLayoutMeasuring(
layoutMeasuring: Arguments['config']
): LayoutMeasuring {
return layoutMeasuring
? {
...defaultLayoutMeasuring,
...layoutMeasuring,
}
: defaultLayoutMeasuring;
}
83 changes: 0 additions & 83 deletions packages/core/src/hooks/utilities/useLayoutRectMap.ts

This file was deleted.

Loading