@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
459 lines (447 loc) • 17.9 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _typeof = require("@babel/runtime/helpers/typeof");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.DragDropContext = DragDropContext;
exports.resetServerContext = resetServerContext;
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
var _react = _interopRequireWildcard(require("react"));
var _useCapturedDimensions = require("../hooks/use-captured-dimensions");
var _useCleanupFn = require("../hooks/use-cleanup-fn");
var _attributes = require("../utils/attributes");
var _findDragHandle = require("../utils/find-drag-handle");
var _getClosestPositionedElement = require("../utils/get-closest-positioned-element");
var _cancelDrag = require("./cancel-drag");
var _draggableLocation = require("./draggable-location");
var _droppableRegistry = require("./droppable-registry");
var _errorBoundary = require("./error-boundary");
var _getDestination = require("./get-destination");
var _useHiddenTextElement = _interopRequireDefault(require("./hooks/use-hidden-text-element"));
var _useKeyboardControls2 = require("./hooks/use-keyboard-controls");
var _usePointerControls = require("./hooks/use-pointer-controls");
var _useStyleMarshal = _interopRequireDefault(require("./hooks/use-style-marshal"));
var _internalContext = require("./internal-context");
var _lifecycleContext = require("./lifecycle-context");
var _liveRegion = require("./live-region");
var _rbdInvariant = require("./rbd-invariant");
var _screenReader = require("./screen-reader");
var _useScheduler2 = require("./use-scheduler");
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != _typeof(e) && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
// eslint-disable-next-line import/no-extraneous-dependencies
/**
* The instance count is used for selectors when querying the document.
*
* Ideally, in the future, this can be removed completely.
*/
var instanceCount = 0;
function resetServerContext() {
instanceCount = 0;
}
function getContextId() {
return "".concat(instanceCount++);
}
function getOffset(args) {
var offsetElement = (0, _getClosestPositionedElement.getClosestPositionedElement)(args);
return {
top: offsetElement.offsetTop,
left: offsetElement.offsetLeft
};
}
function DragDropContext(_ref) {
var children = _ref.children,
_ref$dragHandleUsageI = _ref.dragHandleUsageInstructions,
dragHandleUsageInstructions = _ref$dragHandleUsageI === void 0 ? _screenReader.defaultDragHandleUsageInstructions : _ref$dragHandleUsageI,
nonce = _ref.nonce,
onBeforeCapture = _ref.onBeforeCapture,
onBeforeDragStart = _ref.onBeforeDragStart,
onDragStart = _ref.onDragStart,
onDragUpdate = _ref.onDragUpdate,
onDragEnd = _ref.onDragEnd;
var _useState = (0, _react.useState)(getContextId),
_useState2 = (0, _slicedToArray2.default)(_useState, 1),
contextId = _useState2[0];
(0, _useHiddenTextElement.default)({
contextId: contextId,
text: dragHandleUsageInstructions
});
var lifecycle = (0, _lifecycleContext.useLifecycle)();
var _useScheduler = (0, _useScheduler2.useScheduler)(),
schedule = _useScheduler.schedule,
flush = _useScheduler.flush;
var dragStateRef = (0, _react.useRef)({
isDragging: false
});
var getDragState = (0, _react.useCallback)(function () {
return dragStateRef.current;
}, []);
var droppableRegistry = (0, _droppableRegistry.useDroppableRegistry)();
var getClosestEnabledDraggableLocation = (0, _react.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]);
(0, _react.useEffect)(function () {
/**
* If there is a drag when the context unmounts, cancel it.
*/
return function () {
var _getDragState = getDragState(),
isDragging = _getDragState.isDragging;
if (isDragging) {
(0, _cancelDrag.cancelPointerDrag)();
}
};
}, [getDragState]);
var updateDrag = (0, _react.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 = (0, _getDestination.getActualDestination)({
start: sourceLocation,
target: targetLocation
});
if ((0, _draggableLocation.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 = (0, _screenReader.getProvided)('onDragUpdate', update),
provided = _getProvided.provided,
getMessage = _getProvided.getMessage;
onDragUpdate === null || onDragUpdate === void 0 || onDragUpdate(update, provided);
(0, _liveRegion.announce)(getMessage());
}
if (isImmediate) {
dispatchConsumerLifecycleEvent();
} else {
schedule(dispatchConsumerLifecycleEvent);
}
}, [droppableRegistry, lifecycle, onDragUpdate, schedule]);
var startDrag = (0, _react.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.attributes.dragHandle.draggableId) ? (0, _attributes.getAttribute)(activeElement, _attributes.attributes.dragHandle.draggableId) : null;
var droppableId = start.source.droppableId;
var droppable = droppableRegistry.getEntry({
droppableId: droppableId
});
(0, _rbdInvariant.rbdInvariant)(droppable, "should have entry for droppable '".concat(droppableId, "'"));
dragStateRef.current = {
isDragging: true,
mode: mode,
draggableDimensions: (0, _useCapturedDimensions.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 = (0, _screenReader.getProvided)('onDragStart', start),
provided = _getProvided2.provided,
getMessage = _getProvided2.getMessage;
onDragStart === null || onDragStart === void 0 || onDragStart(start, provided);
(0, _liveRegion.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 = (0, _useCleanupFn.useCleanupFn)();
var stopDrag = (0, _react.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 = (0, _getDestination.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 = (0, _screenReader.getProvided)('onDragEnd', result),
provided = _getProvided3.provided,
getMessage = _getProvided3.getMessage;
onDragEnd(result, provided);
(0, _liveRegion.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 = (0, _findDragHandle.findDragHandle)({
contextId: contextId,
draggableId: draggableId
});
if (!dragHandle) {
return;
}
dragHandle.focus();
});
}
}, [contextId, flush, keyboardCleanupManager, lifecycle, onDragEnd, updateDrag]);
var dragController = (0, _react.useMemo)(function () {
return {
getDragState: getDragState,
startDrag: startDrag,
updateDrag: updateDrag,
stopDrag: stopDrag
};
}, [getDragState, startDrag, stopDrag, updateDrag]);
(0, _usePointerControls.usePointerControls)({
dragController: dragController,
contextId: contextId
});
var _useKeyboardControls = (0, _useKeyboardControls2.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 = (0, _react.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);
(0, _useStyleMarshal.default)({
contextId: contextId,
nonce: nonce
});
return /*#__PURE__*/_react.default.createElement(_errorBoundary.ErrorBoundary, {
contextId: contextId,
dragController: dragController
}, /*#__PURE__*/_react.default.createElement(_lifecycleContext.LifecycleContextProvider, {
lifecycle: lifecycle
}, /*#__PURE__*/_react.default.createElement(_internalContext.DragDropContextProvider, {
contextId: contextId,
getDragState: getDragState,
startKeyboardDrag: startKeyboardDrag,
droppableRegistry: droppableRegistry
}, children)));
}