UNPKG

react-beautiful-dnd-next

Version:

Beautiful and accessible drag and drop for lists with React

347 lines (302 loc) 10.7 kB
// @flow import type { Position } from 'css-box-model'; import { useRef } from 'react'; import invariant from 'tiny-invariant'; import { useMemo, useCallback } from 'use-memo-one'; import type { EventBinding } from '../util/event-types'; import createEventMarshal, { type EventMarshal, } from '../util/create-event-marshal'; import type { Callbacks } from '../drag-handle-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 createPostDragEventPreventer, { type EventPreventer, } from '../util/create-post-drag-event-preventer'; import isSloppyClickThresholdExceeded from '../util/is-sloppy-click-threshold-exceeded'; import preventStandardKeyEvents from '../util/prevent-standard-key-events'; export type Args = {| callbacks: Callbacks, onCaptureStart: (abort: Function) => void, onCaptureEnd: () => void, getDraggableRef: () => ?HTMLElement, getWindow: () => HTMLElement, canStartCapturing: (event: Event) => boolean, getShouldRespectForcePress: () => boolean, |}; export type OnMouseDown = (event: MouseEvent) => void; // Custom event format for force press inputs // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button const primaryButton: number = 0; const noop = () => {}; // shared management of mousedown without needing to call preventDefault() const mouseDownMarshal: EventMarshal = createEventMarshal(); export default function useMouseSensor(args: Args): OnMouseDown { const { canStartCapturing, getWindow, callbacks, // Currently always respecting force press due to safari bug (see below) // getShouldRespectForcePress, onCaptureStart, onCaptureEnd, } = args; const pendingRef = useRef<?Position>(null); const isDraggingRef = useRef<boolean>(false); const unbindWindowEventsRef = useRef<() => void>(noop); const getIsCapturing = useCallback( () => Boolean(pendingRef.current || isDraggingRef.current), [], ); const schedule = useMemo(() => { invariant( !getIsCapturing(), 'Should not recreate scheduler while capturing', ); return createScheduler(callbacks); }, [callbacks, getIsCapturing]); const postDragEventPreventer: EventPreventer = useMemo( () => createPostDragEventPreventer(getWindow), [getWindow], ); const stop = useCallback(() => { if (!getIsCapturing()) { return; } schedule.cancel(); unbindWindowEventsRef.current(); const shouldBlockClick: boolean = isDraggingRef.current; mouseDownMarshal.reset(); if (shouldBlockClick) { postDragEventPreventer.preventNext(); } // resetting refs pendingRef.current = null; isDraggingRef.current = false; // releasing the capture onCaptureEnd(); }, [getIsCapturing, onCaptureEnd, postDragEventPreventer, schedule]); const cancel = useCallback(() => { const wasDragging: boolean = isDraggingRef.current; stop(); if (wasDragging) { callbacks.onCancel(); } }, [callbacks, stop]); const startDragging = useCallback(() => { invariant(!isDraggingRef.current, 'Cannot start a drag while dragging'); const pending: ?Position = pendingRef.current; invariant(pending, 'Cannot start a drag without a pending drag'); pendingRef.current = null; isDraggingRef.current = true; callbacks.onLift({ clientSelection: pending, movementMode: 'FLUID', }); }, [callbacks]); const windowBindings: EventBinding[] = useMemo(() => { invariant( !getIsCapturing(), 'Should not recreate window bindings while capturing', ); const bindings: EventBinding[] = [ { eventName: 'mousemove', fn: (event: MouseEvent) => { const { button, clientX, clientY } = event; if (button !== primaryButton) { return; } const point: Position = { x: clientX, y: clientY, }; // Already dragging if (isDraggingRef.current) { // preventing default as we are using this event event.preventDefault(); schedule.move(point); return; } // There should be a pending drag at this point const pending: ?Position = pendingRef.current; if (!pending) { // this should be an impossible state // we cannot use kill directly as it checks if there is a pending drag stop(); invariant( false, 'Expected there to be an active or pending drag when window mousemove event is received', ); } // threshold not yet exceeded if (!isSloppyClickThresholdExceeded(pending, point)) { return; } // preventing default as we are using this event event.preventDefault(); startDragging(); }, }, { eventName: 'mouseup', fn: (event: MouseEvent) => { const wasDragging: boolean = isDraggingRef.current; stop(); if (wasDragging) { // preventing default as we are using this event event.preventDefault(); callbacks.onDrop(); } }, }, { eventName: 'mousedown', fn: (event: MouseEvent) => { // this can happen during a drag when the user clicks a button // other than the primary mouse button if (isDraggingRef.current) { event.preventDefault(); } cancel(); }, }, { eventName: 'keydown', fn: (event: KeyboardEvent) => { // Abort if any keystrokes while a drag is pending if (pendingRef.current) { stop(); return; } // cancelling a drag if (event.keyCode === keyCodes.escape) { event.preventDefault(); cancel(); return; } preventStandardKeyEvents(event); }, }, { eventName: 'resize', fn: cancel, }, { eventName: 'scroll', // ## Passive: true // Eventual consistency is fine because we use position: fixed on the item // ## Capture: false // 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 // TODO: can result in awkward drop position options: { passive: true, 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; } // stop a pending drag if (pendingRef.current) { stop(); return; } // getCallbacks().onWindowScroll(); schedule.windowScrollMove(); }, }, // Need to opt out of dragging if the user is a force press // Only for safari which has decided to introduce its own custom way of doing things // https://developer.apple.com/library/content/documentation/AppleApplications/Conceptual/SafariJSProgTopics/RespondingtoForceTouchEventsfromJavaScript.html { eventName: 'webkitmouseforcedown', fn: () => { // In order to opt out of force press correctly we need to call // event.preventDefault() on webkitmouseforcewillbegin // We have no way of doing this in this branch so we are always respecting force touches // There is a correct fix in the `virtual` branch cancel(); }, }, // Cancel on page visibility change { eventName: supportedPageVisibilityEventName, fn: cancel, }, ]; return bindings; }, [ getIsCapturing, cancel, startDragging, schedule, stop, callbacks, 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 startPendingDrag = useCallback( (point: Position) => { invariant(!pendingRef.current, 'Expected there to be no pending drag'); pendingRef.current = point; onCaptureStart(stop); bindWindowEvents(); }, [bindWindowEvents, onCaptureStart, stop], ); const onMouseDown = useCallback( (event: MouseEvent) => { if (mouseDownMarshal.isHandled()) { return; } invariant( !getIsCapturing(), 'Should not be able to perform a mouse down while a drag or pending drag is occurring', ); // We do not need to prevent the event on a dropping draggable as // the mouse down event will not fire due to pointer-events: none // https://codesandbox.io/s/oxo0o775rz if (!canStartCapturing(event)) { return; } // only starting a drag if dragging with the primary mouse button if (event.button !== primaryButton) { return; } // Do not start a drag if any modifier key is pressed if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) { return; } // Registering that this event has been handled. // This is to prevent parent draggables using this event // to start also. // Ideally we would not use preventDefault() as we are not sure // if this mouse down is part of a drag interaction // Unfortunately we do to prevent the element obtaining focus (see below). mouseDownMarshal.handle(); // Unfortunately we do need to prevent the drag handle from getting focus on mousedown. // This goes against our policy on not blocking events before a drag has started. // See [How we use dom events](/docs/guides/how-we-use-dom-events.md). event.preventDefault(); const point: Position = { x: event.clientX, y: event.clientY, }; startPendingDrag(point); }, [canStartCapturing, getIsCapturing, startPendingDrag], ); return onMouseDown; }