react-beautiful-dnd-next
Version:
Beautiful and accessible drag and drop for lists with React
237 lines (216 loc) • 6.26 kB
JavaScript
// @flow
import invariant from 'tiny-invariant';
import { useRef } from 'react';
import { useMemo, useCallback } from 'use-memo-one';
import type { Args, DragHandleProps } from './drag-handle-types';
import getWindowFromEl from '../window/get-window-from-el';
import useRequiredContext from '../use-required-context';
import AppContext, { type AppContextValue } from '../context/app-context';
import useMouseSensor, {
type Args as MouseSensorArgs,
} from './sensor/use-mouse-sensor';
import shouldAllowDraggingFromTarget from './util/should-allow-dragging-from-target';
import useKeyboardSensor, {
type Args as KeyboardSensorArgs,
} from './sensor/use-keyboard-sensor';
import useTouchSensor, {
type Args as TouchSensorArgs,
} from './sensor/use-touch-sensor';
import usePreviousRef from '../use-previous-ref';
import { warning } from '../../dev-warning';
import useValidation from './use-validation';
import useFocusRetainer from './use-focus-retainer';
import useLayoutEffect from '../use-isomorphic-layout-effect';
function preventHtml5Dnd(event: DragEvent) {
event.preventDefault();
}
type Capturing = {|
abort: () => void,
|};
export default function useDragHandle(args: Args): ?DragHandleProps {
// Capturing
const capturingRef = useRef<?Capturing>(null);
const onCaptureStart = useCallback((abort: () => void) => {
invariant(
!capturingRef.current,
'Cannot start capturing while something else is',
);
capturingRef.current = {
abort,
};
}, []);
const onCaptureEnd = useCallback(() => {
invariant(
capturingRef.current,
'Cannot stop capturing while nothing is capturing',
);
capturingRef.current = null;
}, []);
const abortCapture = useCallback(() => {
invariant(capturingRef.current, 'Cannot abort capture when there is none');
capturingRef.current.abort();
}, []);
const { canLift, style: styleContext }: AppContextValue = useRequiredContext(
AppContext,
);
const {
isDragging,
isEnabled,
draggableId,
callbacks,
getDraggableRef,
getShouldRespectForcePress,
canDragInteractiveElements,
} = args;
const lastArgsRef = usePreviousRef(args);
useValidation({ isEnabled, getDraggableRef });
const getWindow = useCallback(
(): HTMLElement => getWindowFromEl(getDraggableRef()),
[getDraggableRef],
);
const canStartCapturing = useCallback(
(event: Event) => {
// Cannot lift when disabled
if (!isEnabled) {
return false;
}
// Something on this element might be capturing.
// A drag might not have started yet
// We want to prevent anything else from capturing
if (capturingRef.current) {
return false;
}
// Do not drag if anything else in the system is dragging
if (!canLift(draggableId)) {
return false;
}
// Check if we are dragging an interactive element
return shouldAllowDraggingFromTarget(event, canDragInteractiveElements);
},
[canDragInteractiveElements, canLift, draggableId, isEnabled],
);
const { onBlur, onFocus } = useFocusRetainer(args);
const mouseArgs: MouseSensorArgs = useMemo(
() => ({
callbacks,
getDraggableRef,
getWindow,
canStartCapturing,
onCaptureStart,
onCaptureEnd,
getShouldRespectForcePress,
}),
[
callbacks,
getDraggableRef,
getWindow,
canStartCapturing,
onCaptureStart,
onCaptureEnd,
getShouldRespectForcePress,
],
);
const onMouseDown = useMouseSensor(mouseArgs);
const keyboardArgs: KeyboardSensorArgs = useMemo(
() => ({
callbacks,
getDraggableRef,
getWindow,
canStartCapturing,
onCaptureStart,
onCaptureEnd,
}),
[
callbacks,
canStartCapturing,
getDraggableRef,
getWindow,
onCaptureEnd,
onCaptureStart,
],
);
const onKeyDown = useKeyboardSensor(keyboardArgs);
const touchArgs: TouchSensorArgs = useMemo(
() => ({
callbacks,
getDraggableRef,
getWindow,
canStartCapturing,
getShouldRespectForcePress,
onCaptureStart,
onCaptureEnd,
}),
[
callbacks,
getDraggableRef,
getWindow,
canStartCapturing,
getShouldRespectForcePress,
onCaptureStart,
onCaptureEnd,
],
);
const onTouchStart = useTouchSensor(touchArgs);
// aborting on unmount
useLayoutEffect(() => {
// only when unmounting
return () => {
if (!capturingRef.current) {
return;
}
abortCapture();
if (lastArgsRef.current.isDragging) {
// eslint-disable-next-line react-hooks/exhaustive-deps
lastArgsRef.current.callbacks.onCancel();
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// No longer enabled but still capturing: need to abort and cancel if needed
if (!isEnabled && capturingRef.current) {
abortCapture();
if (lastArgsRef.current.isDragging) {
warning(
'You have disabled dragging on a Draggable while it was dragging. The drag has been cancelled',
);
callbacks.onCancel();
}
}
// Handle aborting
// No longer dragging but still capturing: need to abort
// Using a layout effect to ensure that there is a flip from isDragging => !isDragging
// When there is a pending drag !isDragging will always be true
useLayoutEffect(() => {
if (!isDragging && capturingRef.current) {
abortCapture();
}
}, [abortCapture, isDragging]);
const props: ?DragHandleProps = useMemo(() => {
if (!isEnabled) {
return null;
}
return {
onMouseDown,
onKeyDown,
onTouchStart,
onFocus,
onBlur,
tabIndex: 0,
'data-react-beautiful-dnd-drag-handle': styleContext,
// English default. Consumers are welcome to add their own start instruction
'aria-roledescription': 'Draggable item. Press space bar to lift',
// Opting out of html5 drag and drops
draggable: false,
onDragStart: preventHtml5Dnd,
};
}, [
isEnabled,
onBlur,
onFocus,
onKeyDown,
onMouseDown,
onTouchStart,
styleContext,
]);
return props;
}