UNPKG

@atlaskit/pragmatic-drag-and-drop-react-beautiful-dnd-migration

Version:

An optional Pragmatic drag and drop package that enables rapid migration from react-beautiful-dnd to Pragmatic drag and drop

447 lines (436 loc) 16 kB
import _slicedToArray from "@babel/runtime/helpers/slicedToArray"; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { getDraggableDimensions } from '../hooks/use-captured-dimensions'; import { useCleanupFn } from '../hooks/use-cleanup-fn'; import { attributes, getAttribute } from '../utils/attributes'; import { findDragHandle } from '../utils/find-drag-handle'; import { getClosestPositionedElement } from '../utils/get-closest-positioned-element'; import { cancelPointerDrag } from './cancel-drag'; import { isSameLocation } from './draggable-location'; import { useDroppableRegistry } from './droppable-registry'; import { ErrorBoundary } from './error-boundary'; import { getActualDestination } from './get-destination'; import useHiddenTextElement from './hooks/use-hidden-text-element'; import { useKeyboardControls } from './hooks/use-keyboard-controls'; import { usePointerControls } from './hooks/use-pointer-controls'; import useStyleMarshal from './hooks/use-style-marshal'; import { DragDropContextProvider } from './internal-context'; import { LifecycleContextProvider, useLifecycle } from './lifecycle-context'; import { announce } from './live-region'; import { rbdInvariant } from './rbd-invariant'; import { defaultDragHandleUsageInstructions, getProvided } from './screen-reader'; import { useScheduler } from './use-scheduler'; /** * The instance count is used for selectors when querying the document. * * Ideally, in the future, this can be removed completely. */ var instanceCount = 0; export function resetServerContext() { instanceCount = 0; } function getContextId() { return "".concat(instanceCount++); } function getOffset(args) { var offsetElement = getClosestPositionedElement(args); return { top: offsetElement.offsetTop, left: offsetElement.offsetLeft }; } export function DragDropContext(_ref) { var children = _ref.children, _ref$dragHandleUsageI = _ref.dragHandleUsageInstructions, dragHandleUsageInstructions = _ref$dragHandleUsageI === void 0 ? defaultDragHandleUsageInstructions : _ref$dragHandleUsageI, nonce = _ref.nonce, onBeforeCapture = _ref.onBeforeCapture, onBeforeDragStart = _ref.onBeforeDragStart, onDragStart = _ref.onDragStart, onDragUpdate = _ref.onDragUpdate, onDragEnd = _ref.onDragEnd; var _useState = useState(getContextId), _useState2 = _slicedToArray(_useState, 1), contextId = _useState2[0]; useHiddenTextElement({ contextId: contextId, text: dragHandleUsageInstructions }); var lifecycle = useLifecycle(); var _useScheduler = useScheduler(), schedule = _useScheduler.schedule, flush = _useScheduler.flush; var dragStateRef = useRef({ isDragging: false }); var getDragState = useCallback(function () { return dragStateRef.current; }, []); var droppableRegistry = useDroppableRegistry(); var getClosestEnabledDraggableLocation = useCallback(function (_ref2) { var droppableId = _ref2.droppableId; var droppable = droppableRegistry.getEntry({ droppableId: droppableId }); while (droppable !== null && droppable.isDropDisabled) { var _droppable = droppable, parentDroppableId = _droppable.parentDroppableId; droppable = parentDroppableId ? droppableRegistry.getEntry({ droppableId: parentDroppableId }) : null; } if (droppable === null) { return null; } return { droppableId: droppable.droppableId, index: 0 }; }, [droppableRegistry]); useEffect(function () { /** * If there is a drag when the context unmounts, cancel it. */ return function () { var _getDragState = getDragState(), isDragging = _getDragState.isDragging; if (isDragging) { cancelPointerDrag(); } }; }, [getDragState]); var updateDrag = useCallback(function (_ref3) { var targetLocation = _ref3.targetLocation, _ref3$isImmediate = _ref3.isImmediate, isImmediate = _ref3$isImmediate === void 0 ? false : _ref3$isImmediate; if (!dragStateRef.current.isDragging) { /** * If there is no ongoing drag, then don't do anything. * * This should never occur, but treating it as a noop is more * reasonable than an invariant. */ return; } var _dragStateRef$current = dragStateRef.current, prevDestination = _dragStateRef$current.prevDestination, draggableId = _dragStateRef$current.draggableId, type = _dragStateRef$current.type, sourceLocation = _dragStateRef$current.sourceLocation; /** * Computes where it would actually move to */ var nextDestination = getActualDestination({ start: sourceLocation, target: targetLocation }); if (isSameLocation(prevDestination, nextDestination)) { return; } Object.assign(dragStateRef.current, { prevDestination: nextDestination, sourceLocation: sourceLocation, targetLocation: targetLocation }); var update = { mode: dragStateRef.current.mode, draggableId: draggableId, type: type, source: sourceLocation, destination: nextDestination, combine: null // not supported by migration layer }; var droppable = targetLocation ? droppableRegistry.getEntry({ droppableId: targetLocation.droppableId }) : null; /** * This event exists solely to ensure that the drop indicator updates * before the drag preview. */ lifecycle.dispatch('onPrePendingDragUpdate', { update: update, targetLocation: targetLocation }); lifecycle.dispatch('onPendingDragUpdate', { update: update, targetLocation: targetLocation, droppable: droppable }); function dispatchConsumerLifecycleEvent() { var _getProvided = getProvided('onDragUpdate', update), provided = _getProvided.provided, getMessage = _getProvided.getMessage; onDragUpdate === null || onDragUpdate === void 0 || onDragUpdate(update, provided); announce(getMessage()); } if (isImmediate) { dispatchConsumerLifecycleEvent(); } else { schedule(dispatchConsumerLifecycleEvent); } }, [droppableRegistry, lifecycle, onDragUpdate, schedule]); var startDrag = useCallback(function (_ref4) { var draggableId = _ref4.draggableId, type = _ref4.type, getSourceLocation = _ref4.getSourceLocation, sourceElement = _ref4.sourceElement, mode = _ref4.mode; if (dragStateRef.current.isDragging) { /** * If there is already an ongoing drag, then don't do anything. * * This should never occur, but treating it as a noop is more * reasonable than an invariant. */ return; } var before = { draggableId: draggableId, mode: mode }; // This is called in `onDragStart` rather than `onGenerateDragPreview` // to avoid a browser bug. Some DOM manipulations can cancel // the drag if they happen early in the drag. // <https://bugs.chromium.org/p/chromium/issues/detail?id=674882> onBeforeCapture === null || onBeforeCapture === void 0 || onBeforeCapture(before); var start = { mode: mode, draggableId: draggableId, type: type, source: getSourceLocation() }; /** * If the active element is a drag handle, then * we want to restore focus to it after the drag. * * This matches the behavior of `react-beautiful-dnd`. */ var _document = document, activeElement = _document.activeElement; var dragHandleDraggableId = activeElement instanceof HTMLElement && activeElement.hasAttribute(attributes.dragHandle.draggableId) ? getAttribute(activeElement, attributes.dragHandle.draggableId) : null; var droppableId = start.source.droppableId; var droppable = droppableRegistry.getEntry({ droppableId: droppableId }); rbdInvariant(droppable, "should have entry for droppable '".concat(droppableId, "'")); dragStateRef.current = { isDragging: true, mode: mode, draggableDimensions: getDraggableDimensions(sourceElement), restoreFocusTo: dragHandleDraggableId, draggableId: draggableId, type: type, prevDestination: start.source, sourceLocation: start.source, targetLocation: start.source, draggableInitialOffsetInSourceDroppable: getOffset({ element: sourceElement, mode: droppable.mode }) }; onBeforeDragStart === null || onBeforeDragStart === void 0 || onBeforeDragStart(start); /** * This is used to signal to <Draggable> and <Droppable> elements * to update their state. * * This must be synchronous so that they have updated their state * by the time that `DragStart` is published. */ lifecycle.dispatch('onPendingDragStart', { start: start, droppable: droppable }); // rbd's `onDragStart` is called in the next event loop (via `setTimeout`) // // We can safely assume that the React state updates have occurred by // now, and that the updated `snapshot` has been provided. // <https://twitter.com/alexandereardon/status/1585784101885263872> schedule(function () { var start = { mode: mode, draggableId: draggableId, type: type, source: getSourceLocation() }; var _getProvided2 = getProvided('onDragStart', start), provided = _getProvided2.provided, getMessage = _getProvided2.getMessage; onDragStart === null || onDragStart === void 0 || onDragStart(start, provided); announce(getMessage()); /** * If the droppable is initially disabled, then we publish an * immediate `DragUpdate` with a new non-disabled destination. * * This is typically `destination === null` but can be a parent * droppable if there are nested droppables. * * `react-beautiful-dnd` does this for mouse drags, * but not for keyboard drags. This is likely a bug, and the migration * layer will publish an update for all types of drags. * * This is scheduled so that state changes that occurred in the * rbd `onDragStart` will have taken effect. That is, * a synchronous `setIsDropDisabled(true)` call in the consumer's * `onDragStart` should result in an immediate update here. */ schedule(function () { var droppableId = start.source.droppableId; var droppable = droppableRegistry.getEntry({ droppableId: droppableId }); if (droppable !== null && droppable !== void 0 && droppable.isDropDisabled) { var targetLocation = getClosestEnabledDraggableLocation({ droppableId: droppableId }); updateDrag({ targetLocation: targetLocation, isImmediate: true }); } }); }); }, [droppableRegistry, getClosestEnabledDraggableLocation, lifecycle, onBeforeCapture, onBeforeDragStart, onDragStart, schedule, updateDrag]); var keyboardCleanupManager = useCleanupFn(); var stopDrag = useCallback(function (_ref5) { var reason = _ref5.reason; if (!dragStateRef.current.isDragging) { /** * If there is no ongoing drag, then don't do anything. * * This should never occur, but treating it as a noop is more * reasonable than an invariant. */ return; } keyboardCleanupManager.runCleanupFn(); /** * If this is a cancel, then an update to a null * destination will be made. (Unless it is already null) * * This is different to `react-beautiful-dnd` and exists * to standardize behavior between mouse and keyboard drags. * * This is required because of a behavior in native drag and * drop, where a `dragend` will fire exit events on every * drop target you are over. This results in an unavoidable * null destination update for mouse drags. */ if (reason === 'CANCEL') { updateDrag({ targetLocation: null }); } var _dragStateRef$current2 = dragStateRef.current, mode = _dragStateRef$current2.mode, restoreFocusTo = _dragStateRef$current2.restoreFocusTo, sourceLocation = _dragStateRef$current2.sourceLocation, targetLocation = _dragStateRef$current2.targetLocation, type = _dragStateRef$current2.type, draggableId = _dragStateRef$current2.draggableId; dragStateRef.current = { isDragging: false }; flush(); var destination = getActualDestination({ start: sourceLocation, target: targetLocation }); var result = { // We are saying all null destination drops count as a CANCEL reason: destination === null ? 'CANCEL' : 'DROP', type: type, source: sourceLocation, destination: destination, mode: mode, draggableId: draggableId, combine: null // not supported by migration layer }; /** * Tells <Draggable> instances to cleanup. */ lifecycle.dispatch('onBeforeDragEnd', { draggableId: draggableId }); var _getProvided3 = getProvided('onDragEnd', result), provided = _getProvided3.provided, getMessage = _getProvided3.getMessage; onDragEnd(result, provided); announce(getMessage()); if (restoreFocusTo) { /** * The `requestAnimationFrame` matches `react-beautiful-dnd`. * * It is required to wait for React state updates to have taken effect. * Otherwise we might try to focus an element that no longer exists. */ requestAnimationFrame(function () { var dragHandle = findDragHandle({ contextId: contextId, draggableId: draggableId }); if (!dragHandle) { return; } dragHandle.focus(); }); } }, [contextId, flush, keyboardCleanupManager, lifecycle, onDragEnd, updateDrag]); var dragController = useMemo(function () { return { getDragState: getDragState, startDrag: startDrag, updateDrag: updateDrag, stopDrag: stopDrag }; }, [getDragState, startDrag, stopDrag, updateDrag]); usePointerControls({ dragController: dragController, contextId: contextId }); var _useKeyboardControls = useKeyboardControls({ dragController: dragController, droppableRegistry: droppableRegistry, contextId: contextId, setKeyboardCleanupFn: keyboardCleanupManager.setCleanupFn }), startKeyboardDrag = _useKeyboardControls.startKeyboardDrag; /** * If a droppable becomes disabled during a drag, then a new destination * should be found and published in a `DragUpdate`. */ var onDroppableUpdate = useCallback(function (entry) { var _dragState$targetLoca; var dragState = dragStateRef.current; if (!dragState.isDragging) { return; } if (!entry.isDropDisabled) { return; } if (entry.droppableId !== ((_dragState$targetLoca = dragState.targetLocation) === null || _dragState$targetLoca === void 0 ? void 0 : _dragState$targetLoca.droppableId)) { return; } var targetLocation = getClosestEnabledDraggableLocation({ droppableId: entry.droppableId }); updateDrag({ targetLocation: targetLocation }); }, [getClosestEnabledDraggableLocation, updateDrag]); droppableRegistry.setUpdateListener(onDroppableUpdate); useStyleMarshal({ contextId: contextId, nonce: nonce }); return /*#__PURE__*/React.createElement(ErrorBoundary, { contextId: contextId, dragController: dragController }, /*#__PURE__*/React.createElement(LifecycleContextProvider, { lifecycle: lifecycle }, /*#__PURE__*/React.createElement(DragDropContextProvider, { contextId: contextId, getDragState: getDragState, startKeyboardDrag: startKeyboardDrag, droppableRegistry: droppableRegistry }, children))); }