react-beautiful-dnd-next
Version:
Beautiful and accessible drag and drop for lists with React
280 lines (247 loc) • 8.7 kB
JavaScript
// @flow
import { useRef } from 'react';
import invariant from 'tiny-invariant';
import { type Position } from 'css-box-model';
import rafSchedule from 'raf-schd';
import { useMemo, useCallback } from 'use-memo-one';
import memoizeOne from 'memoize-one';
import checkForNestedScrollContainers from './check-for-nested-scroll-container';
import { origin } from '../../state/position';
import getScroll from './get-scroll';
import type {
DimensionMarshal,
DroppableCallbacks,
RecollectDroppableOptions,
} from '../../state/dimension-marshal/dimension-marshal-types';
import getEnv, { type Env } from './get-env';
import type {
DroppableId,
TypeId,
DroppableDimension,
DroppableDescriptor,
Direction,
ScrollOptions,
} from '../../types';
import getDimension from './get-dimension';
import AppContext, { type AppContextValue } from '../context/app-context';
import withoutPlaceholder from './without-placeholder';
import { warning } from '../../dev-warning';
import getListenerOptions from './get-listener-options';
import useRequiredContext from '../use-required-context';
import usePreviousRef from '../use-previous-ref';
import useLayoutEffect from '../use-isomorphic-layout-effect';
type Props = {|
droppableId: DroppableId,
type: TypeId,
direction: Direction,
isDropDisabled: boolean,
isCombineEnabled: boolean,
ignoreContainerClipping: boolean,
getPlaceholderRef: () => ?HTMLElement,
getDroppableRef: () => ?HTMLElement,
|};
type WhileDragging = {|
ref: HTMLElement,
descriptor: DroppableDescriptor,
env: Env,
scrollOptions: ScrollOptions,
|};
const getClosestScrollableFromDrag = (dragging: ?WhileDragging): ?Element =>
(dragging && dragging.env.closestScrollable) || null;
export default function useDroppableDimensionPublisher(args: Props) {
const whileDraggingRef = useRefWhileDragging>(null);
const appContext: AppContextValue = useRequiredContext(AppContext);
const marshal: DimensionMarshal = appContext.marshal;
const previousRef = usePreviousRef(args);
const descriptor = useMemo<DroppableDescriptor>(() => {
return {
id: args.droppableId,
type: args.type,
};
}, [args.droppableId, args.type]);
const publishedDescriptorRef = useRef<DroppableDescriptor>(descriptor);
const memoizedUpdateScroll = useMemo(
() =>
memoizeOne((x: number, y: number) => {
invariant(
whileDraggingRef.current,
'Can only update scroll when dragging',
);
const scroll: Position = { x, y };
marshal.updateDroppableScroll(descriptor.id, scroll);
}),
[descriptor.id, marshal],
);
const getClosestScroll = useCallback((): Position => {
const dragging: ?WhileDragging = whileDraggingRef.current;
if (!dragging || !dragging.env.closestScrollable) {
return origin;
}
return getScroll(dragging.env.closestScrollable);
}, []);
const updateScroll = useCallback(() => {
const scroll: Position = getClosestScroll();
memoizedUpdateScroll(scroll.x, scroll.y);
}, [getClosestScroll, memoizedUpdateScroll]);
const scheduleScrollUpdate = useMemo(() => rafSchedule(updateScroll), [
updateScroll,
]);
const onClosestScroll = useCallback(() => {
const dragging: ?WhileDragging = whileDraggingRef.current;
const closest: ?Element = getClosestScrollableFromDrag(dragging);
invariant(
dragging && closest,
'Could not find scroll options while scrolling',
);
const options: ScrollOptions = dragging.scrollOptions;
if (options.shouldPublishImmediately) {
updateScroll();
return;
}
scheduleScrollUpdate();
}, [scheduleScrollUpdate, updateScroll]);
const getDimensionAndWatchScroll = useCallback(
(windowScroll: Position, options: ScrollOptions) => {
invariant(
!whileDraggingRef.current,
'Cannot collect a droppable while a drag is occurring',
);
const previous: Props = previousRef.current;
const ref: ?HTMLElement = previous.getDroppableRef();
invariant(ref, 'Cannot collect without a droppable ref');
const env: Env = getEnv(ref);
const dragging: WhileDragging = {
ref,
descriptor,
env,
scrollOptions: options,
};
// side effect
whileDraggingRef.current = dragging;
const dimension: DroppableDimension = getDimension({
ref,
descriptor,
env,
windowScroll,
direction: previous.direction,
isDropDisabled: previous.isDropDisabled,
isCombineEnabled: previous.isCombineEnabled,
shouldClipSubject: !previous.ignoreContainerClipping,
});
if (env.closestScrollable) {
// bind scroll listener
env.closestScrollable.addEventListener(
'scroll',
onClosestScroll,
getListenerOptions(dragging.scrollOptions),
);
// print a debug warning if using an unsupported nested scroll container setup
if (process.env.NODE_ENV !== 'production') {
checkForNestedScrollContainers(env.closestScrollable);
}
}
return dimension;
},
[descriptor, onClosestScroll, previousRef],
);
const recollect = useCallback(
(options: RecollectDroppableOptions): DroppableDimension => {
const dragging: ?WhileDragging = whileDraggingRef.current;
const closest: ?Element = getClosestScrollableFromDrag(dragging);
invariant(
dragging && closest,
'Can only recollect Droppable client for Droppables that have a scroll container',
);
const previous: Props = previousRef.current;
const execute = (): DroppableDimension =>
getDimension({
ref: dragging.ref,
descriptor: dragging.descriptor,
env: dragging.env,
windowScroll: origin,
direction: previous.direction,
isDropDisabled: previous.isDropDisabled,
isCombineEnabled: previous.isCombineEnabled,
shouldClipSubject: !previous.ignoreContainerClipping,
});
if (!options.withoutPlaceholder) {
return execute();
}
return withoutPlaceholder(previous.getPlaceholderRef(), execute);
},
[previousRef],
);
const dragStopped = useCallback(() => {
const dragging: ?WhileDragging = whileDraggingRef.current;
invariant(dragging, 'Cannot stop drag when no active drag');
const closest: ?Element = getClosestScrollableFromDrag(dragging);
// goodbye old friend
whileDraggingRef.current = null;
if (!closest) {
return;
}
// unwatch scroll
scheduleScrollUpdate.cancel();
closest.removeEventListener(
'scroll',
onClosestScroll,
getListenerOptions(dragging.scrollOptions),
);
}, [onClosestScroll, scheduleScrollUpdate]);
const scroll = useCallback((change: Position) => {
// arrange
const dragging: ?WhileDragging = whileDraggingRef.current;
invariant(dragging, 'Cannot scroll when there is no drag');
const closest: ?Element = getClosestScrollableFromDrag(dragging);
invariant(closest, 'Cannot scroll a droppable with no closest scrollable');
// act
closest.scrollTop += change.y;
closest.scrollLeft += change.x;
}, []);
const callbacks: DroppableCallbacks = useMemo(() => {
return {
getDimensionAndWatchScroll,
recollect,
dragStopped,
scroll,
};
}, [dragStopped, getDimensionAndWatchScroll, recollect, scroll]);
// Register with the marshal and let it know of:
// - any descriptor changes
// - when it unmounts
useLayoutEffect(() => {
publishedDescriptorRef.current = descriptor;
marshal.registerDroppable(descriptor, callbacks);
return () => {
if (whileDraggingRef.current) {
warning(
'Unsupported: changing the droppableId or type of a Droppable during a drag',
);
dragStopped();
}
marshal.unregisterDroppable(descriptor);
};
}, [callbacks, descriptor, dragStopped, marshal]);
// update is enabled with the marshal
// only need to update when there is a drag
useLayoutEffect(() => {
if (!whileDraggingRef.current) {
return;
}
marshal.updateDroppableIsEnabled(
publishedDescriptorRef.current.id,
!args.isDropDisabled,
);
}, [args.isDropDisabled, marshal]);
// update is combine enabled with the marshal
// only need to update when there is a drag
useLayoutEffect(() => {
if (!whileDraggingRef.current) {
return;
}
marshal.updateDroppableIsCombineEnabled(
publishedDescriptorRef.current.id,
args.isCombineEnabled,
);
}, [args.isCombineEnabled, marshal]);
}