@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
JavaScript
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)));
}