UNPKG

react-beautiful-dnd-next

Version:

Beautiful and accessible drag and drop for lists with React

264 lines (228 loc) 7.12 kB
// @flow import type { Position } from 'css-box-model'; import { useRef } from 'react'; import { useMemo, useCallback } from 'use-memo-one'; import invariant from 'tiny-invariant'; import type { EventBinding } from '../util/event-types'; import { bindEvents, unbindEvents } from '../util/bind-events'; import createScheduler from '../util/create-scheduler'; import * as keyCodes from '../../key-codes'; import supportedPageVisibilityEventName from '../util/supported-page-visibility-event-name'; import preventStandardKeyEvents from '../util/prevent-standard-key-events'; import type { Callbacks } from '../drag-handle-types'; import getBorderBoxCenterPosition from '../../get-border-box-center-position'; export type Args = {| callbacks: Callbacks, getDraggableRef: () => ?HTMLElement, getWindow: () => HTMLElement, canStartCapturing: (event: Event) => boolean, onCaptureStart: (abort: () => void) => void, onCaptureEnd: () => void, |}; export type OnKeyDown = (event: KeyboardEvent) => void; type KeyMap = { [key: number]: true, }; const scrollJumpKeys: KeyMap = { [keyCodes.pageDown]: true, [keyCodes.pageUp]: true, [keyCodes.home]: true, [keyCodes.end]: true, }; function noop() {} export default function useKeyboardSensor(args: Args): OnKeyDown { const { canStartCapturing, getWindow, callbacks, onCaptureStart, onCaptureEnd, getDraggableRef, } = args; const isDraggingRef = useRef<boolean>(false); const unbindWindowEventsRef = useRef<() => void>(noop); const getIsDragging = useCallback(() => isDraggingRef.current, []); const schedule = useMemo(() => { invariant( !getIsDragging(), 'Should not recreate scheduler while capturing', ); return createScheduler(callbacks); }, [callbacks, getIsDragging]); const stop = useCallback(() => { if (!getIsDragging()) { return; } schedule.cancel(); unbindWindowEventsRef.current(); isDraggingRef.current = false; onCaptureEnd(); }, [getIsDragging, onCaptureEnd, schedule]); const cancel = useCallback(() => { const wasDragging: boolean = isDraggingRef.current; stop(); if (wasDragging) { callbacks.onCancel(); } }, [callbacks, stop]); const windowBindings: EventBinding[] = useMemo(() => { invariant( !getIsDragging(), 'Should not recreate window bindings when dragging', ); return [ // any mouse actions kills a drag { eventName: 'mousedown', fn: cancel, }, { eventName: 'mouseup', fn: cancel, }, { eventName: 'click', fn: cancel, }, { eventName: 'touchstart', fn: cancel, }, // resizing the browser kills a drag { eventName: 'resize', fn: cancel, }, // kill if the user is using the mouse wheel // We are not supporting wheel / trackpad scrolling with keyboard dragging { eventName: 'wheel', fn: cancel, // chrome says it is a violation for this to not be passive // it is fine for it to be passive as we just cancel as soon as we get // any event options: { passive: true }, }, // Need to respond instantly to a jump scroll request // Not using the scheduler { eventName: 'scroll', // Scroll events on elements do not bubble, but they go through the capture phase // https://twitter.com/alexandereardon/status/985994224867819520 // Using capture: false here as we want to avoid intercepting droppable scroll requests options: { capture: false }, fn: (event: UIEvent) => { // IE11 fix: // Scrollable events still bubble up and are caught by this handler in ie11. // We can ignore this event if (event.currentTarget !== getWindow()) { return; } callbacks.onWindowScroll(); }, }, // Cancel on page visibility change { eventName: supportedPageVisibilityEventName, fn: cancel, }, ]; }, [callbacks, cancel, getIsDragging, getWindow]); const bindWindowEvents = useCallback(() => { const win: HTMLElement = getWindow(); const options = { capture: true }; // setting up our unbind before we bind unbindWindowEventsRef.current = () => unbindEvents(win, windowBindings, options); bindEvents(win, windowBindings, options); }, [getWindow, windowBindings]); const startDragging = useCallback(() => { invariant(!isDraggingRef.current, 'Cannot start a drag while dragging'); const ref: ?HTMLElement = getDraggableRef(); invariant(ref, 'Cannot start a keyboard drag without a draggable ref'); isDraggingRef.current = true; onCaptureStart(stop); bindWindowEvents(); const center: Position = getBorderBoxCenterPosition(ref); callbacks.onLift({ clientSelection: center, movementMode: 'SNAP', }); }, [bindWindowEvents, callbacks, getDraggableRef, onCaptureStart, stop]); const onKeyDown: OnKeyDown = useCallback( (event: KeyboardEvent) => { // not dragging yet if (!getIsDragging()) { // We may already be lifting on a child draggable. // We do not need to use an EventMarshal here as // we always call preventDefault on the first input if (event.defaultPrevented) { return; } // Cannot lift at this time if (!canStartCapturing(event)) { return; } if (event.keyCode !== keyCodes.space) { return; } // Calling preventDefault as we are consuming the event event.preventDefault(); startDragging(); return; } // already dragging // Cancelling if (event.keyCode === keyCodes.escape) { event.preventDefault(); cancel(); return; } // Dropping if (event.keyCode === keyCodes.space) { // need to stop parent Draggable's thinking this is a lift event.preventDefault(); stop(); callbacks.onDrop(); return; } // Movement if (event.keyCode === keyCodes.arrowDown) { event.preventDefault(); schedule.moveDown(); return; } if (event.keyCode === keyCodes.arrowUp) { event.preventDefault(); schedule.moveUp(); return; } if (event.keyCode === keyCodes.arrowRight) { event.preventDefault(); schedule.moveRight(); return; } if (event.keyCode === keyCodes.arrowLeft) { event.preventDefault(); schedule.moveLeft(); return; } // preventing scroll jumping at this time if (scrollJumpKeys[event.keyCode]) { event.preventDefault(); return; } preventStandardKeyEvents(event); }, [ callbacks, canStartCapturing, cancel, getIsDragging, schedule, startDragging, stop, ], ); return onKeyDown; }