@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
462 lines (450 loc) • 17.2 kB
JavaScript
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import React, { useCallback, useEffect, useMemo, useReducer, useRef } from 'react';
import { bindAll } from 'bind-event-listener';
// eslint-disable-next-line import/no-extraneous-dependencies
import invariant from 'tiny-invariant';
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
import { draggable, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import { disableNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview';
import { getElementFromPointWithoutHoneypot } from '@atlaskit/pragmatic-drag-and-drop/private/get-element-from-point-without-honey-pot';
import { getHiddenTextElementId } from '../drag-drop-context/hooks/use-hidden-text-element';
import { useDragDropContext } from '../drag-drop-context/internal-context';
import { useMonitorForLifecycle } from '../drag-drop-context/lifecycle-context';
import { rbdInvariant } from '../drag-drop-context/rbd-invariant';
import { useDroppableContext } from '../droppable/droppable-context';
import { useDraggableDimensions } from '../hooks/use-captured-dimensions';
import { useCleanupFn } from '../hooks/use-cleanup-fn';
import { useDropTargetForDraggable } from '../hooks/use-drop-target-for-draggable';
import { useKeyboardContext } from '../hooks/use-keyboard-context';
import { attributes, customAttributes, setAttributes } from '../utils/attributes';
import { findDragHandle } from '../utils/find-drag-handle';
import { findDropIndicator } from '../utils/find-drop-indicator';
import { findPlaceholder } from '../utils/find-placeholder';
import { useStable } from '../utils/use-stable';
import { isDraggableData, useDraggableData } from './data';
import { getDraggableProvidedStyle } from './get-draggable-provided-style';
import isEventInInteractiveElement, { isAnInteractiveElement } from './is-event-in-interactive-element';
import { Placeholder } from './placeholder';
import { idleState, reducer } from './state';
import { useDraggableStateSnapshot } from './use-draggable-state-snapshot';
var noop = function noop() {};
export function Draggable(_ref) {
var children = _ref.children,
draggableId = _ref.draggableId,
index = _ref.index,
_ref$isDragDisabled = _ref.isDragDisabled,
isDragDisabled = _ref$isDragDisabled === void 0 ? false : _ref$isDragDisabled,
_ref$disableInteracti = _ref.disableInteractiveElementBlocking,
disableInteractiveElementBlocking = _ref$disableInteracti === void 0 ? false : _ref$disableInteracti;
var _useDroppableContext = useDroppableContext(),
direction = _useDroppableContext.direction,
droppableId = _useDroppableContext.droppableId,
type = _useDroppableContext.type,
mode = _useDroppableContext.mode;
var _useDragDropContext = useDragDropContext(),
contextId = _useDragDropContext.contextId,
getDragState = _useDragDropContext.getDragState;
var elementRef = useRef(null);
var dragHandleRef = useRef(null);
var _useCleanupFn = useCleanupFn(),
setCleanupFn = _useCleanupFn.setCleanupFn,
runCleanupFn = _useCleanupFn.runCleanupFn;
var setElement = useCallback(function (element) {
if (elementRef.current) {
/**
* Call the `setAttribute` clean up if the element changes
*/
runCleanupFn();
}
if (element) {
/**
* The migration layer attaches some additional data attributes.
*
* These are required for querying elements in the DOM.
*
* These are not applied through render props, to avoid changing the type
* interface of the migration layer.
*/
var cleanupFn = setAttributes(element, _defineProperty(_defineProperty({}, customAttributes.draggable.droppableId, droppableId), customAttributes.draggable.index, String(index)));
setCleanupFn(cleanupFn);
}
elementRef.current = element;
dragHandleRef.current = findDragHandle({
contextId: contextId,
draggableId: draggableId
});
}, [contextId, draggableId, droppableId, index, runCleanupFn, setCleanupFn]);
var getIndex = useStable(index);
var _useReducer = useReducer(reducer, idleState),
_useReducer2 = _slicedToArray(_useReducer, 2),
state = _useReducer2[0],
dispatch = _useReducer2[1];
var data = useDraggableData({
draggableId: draggableId,
droppableId: droppableId,
getIndex: getIndex,
contextId: contextId,
type: type
});
var isDragging = state.type === 'dragging';
var isHiding = state.type === 'hiding';
var _useDroppableContext2 = useDroppableContext(),
shouldRenderCloneWhileDragging = _useDroppableContext2.shouldRenderCloneWhileDragging,
isDropDisabled = _useDroppableContext2.isDropDisabled;
var monitorForLifecycle = useMonitorForLifecycle();
var _useKeyboardContext = useKeyboardContext(),
startKeyboardDrag = _useKeyboardContext.startKeyboardDrag;
/**
* Binds the `keydown` listener to the drag handle which handles starting
* keyboard drags.
*/
useEffect(function () {
if (state.type !== 'idle') {
return;
}
if (isDragDisabled) {
return;
}
var element = elementRef.current;
invariant(element instanceof HTMLElement);
var dragHandle = dragHandleRef.current;
invariant(dragHandle instanceof HTMLElement);
return bindAll(dragHandle, [{
type: 'keydown',
listener: function listener(event) {
if (event.key === ' ') {
if (event.defaultPrevented) {
return;
}
if (!disableInteractiveElementBlocking && isEventInInteractiveElement(element, event)) {
return;
}
// Only prevent default if we are consuming it
event.preventDefault();
startKeyboardDrag({
event: event,
draggableId: draggableId,
type: type,
getSourceLocation: function getSourceLocation() {
return {
droppableId: droppableId,
index: getIndex()
};
},
sourceElement: element
});
}
}
}]);
}, [disableInteractiveElementBlocking, draggableId, droppableId, getIndex, isDragDisabled, startKeyboardDrag, state.type, type]);
/**
* Sets up the pdnd draggable.
*/
useEffect(function () {
if (isHiding) {
/**
* If we render a clone, then we need to unmount the original element.
*
* Because of this, `elementRef.current` will become `null` and we will
* no longer have a valid `element` reference.
*
* In this case, not having a valid `element` is expected,
* instead of being an error.
*/
return;
}
if (isDragDisabled) {
return;
}
var element = elementRef.current;
rbdInvariant(element instanceof HTMLElement);
var dragHandle = dragHandleRef.current;
rbdInvariant(dragHandle instanceof HTMLElement);
return draggable({
canDrag: function canDrag(_ref2) {
var input = _ref2.input;
/**
* Do not start a drag if any modifier key is pressed.
* This matches the behavior of `react-beautiful-dnd`.
*/
if (input.ctrlKey || input.metaKey || input.shiftKey || input.altKey) {
return false;
}
/**
* To align with `react-beautiful-dnd` we are blocking drags
* on interactive elements, unless the `disableInteractiveElementBlocking`
* prop is provided.
*/
if (!disableInteractiveElementBlocking) {
var elementUnderPointer = getElementFromPointWithoutHoneypot({
x: input.clientX,
y: input.clientY
});
return !isAnInteractiveElement(dragHandle, elementUnderPointer);
}
return !isDragging;
},
element: element,
dragHandle: dragHandle,
getInitialData: function getInitialData() {
return data;
},
onGenerateDragPreview: disableNativeDragPreview
});
}, [data, disableInteractiveElementBlocking, isDragDisabled, isDragging, isHiding]);
var hasPlaceholder = state.type !== 'idle' && mode === 'standard';
var placeholderRef = useRef(null);
useDropTargetForDraggable({
/**
* Swapping the drop target to the placeholder is important
* to ensure that hovering over where the item was won't result in a
* drop at the end of the list.
*/
elementRef: hasPlaceholder ? placeholderRef : elementRef,
data: data,
direction: direction,
contextId: contextId,
isDropDisabled: isDropDisabled,
type: type
});
var isMountedRef = useRef(true);
useEffect(function () {
/**
* React 18 strict mode will re-run effects in development mode.
* https://react.dev/reference/react/StrictMode#fixing-bugs-found-by-re-running-effects-in-development
*
* Setting the ref value to `true` again in the effect to avoid the value staying `false` incorrectly after
* the first cleanup.
*/
isMountedRef.current = true;
return function () {
isMountedRef.current = false;
};
}, []);
/**
* If the draggable (re)mounts while it is being dragged (via a clone),
* then it should hide itself.
*/
useEffect(function () {
var dragState = getDragState();
/**
* If the draggable is not using a clone, then it doesn't need to be hidden.
*/
if (!shouldRenderCloneWhileDragging) {
return;
}
/**
* If there is no ongoing drag, then it doesn't need to be hidden.
*/
if (!dragState.isDragging) {
return;
}
/**
* Only the draggable being dragged (via a clone) needs to be hidden.
*/
if (dragState.draggableId !== data.draggableId) {
return;
}
dispatch({
type: 'START_HIDING',
payload: {
mode: dragState.mode
}
});
}, [data.draggableId, getDragState, shouldRenderCloneWhileDragging]);
var draggableDimensions = useDraggableDimensions();
useEffect(function () {
/**
* If the draggable should render a clone while dragging,
* then it doesn't need to track any state, and it should be hidden.
*/
if (shouldRenderCloneWhileDragging) {
return monitorForLifecycle({
onPendingDragStart: function onPendingDragStart(_ref3) {
var start = _ref3.start;
if (data.draggableId !== start.draggableId) {
return;
}
dispatch({
type: 'START_HIDING',
payload: {
mode: start.mode
}
});
},
onBeforeDragEnd: function onBeforeDragEnd(_ref4) {
var draggableId = _ref4.draggableId;
if (draggableId !== data.draggableId) {
return;
}
dispatch({
type: 'STOP_HIDING'
});
}
});
}
/**
* Drag events need to be monitored independently because the original
* element can be unmounted for two (valid) reasons.
*
* The original element can be unmounted during the drag for two reasons:
*
* 1. A `renderClone` method has been provided to the containing
* `<Droppable />` element. In this case the element is unmounted so
* that it is not visible while the clone is.
*
* 2. The user portals the element while it is being dragged. This would
* result in the original `HTMLElement` being unmounted.
*/
return combine(monitorForLifecycle({
onPendingDragStart: function onPendingDragStart(_ref5) {
var start = _ref5.start,
droppable = _ref5.droppable;
if (data.draggableId !== start.draggableId) {
return;
}
if (start.mode === 'FLUID') {
return dispatch({
type: 'START_POINTER_DRAG',
payload: {
start: start
}
});
}
if (start.mode === 'SNAP') {
var dragState = getDragState();
rbdInvariant(dragState.isDragging && dragState.draggableDimensions);
return dispatch({
type: 'START_KEYBOARD_DRAG',
payload: {
start: start,
draggableDimensions: dragState.draggableDimensions,
droppable: droppable
}
});
}
},
onPendingDragUpdate: function onPendingDragUpdate(_ref6) {
var update = _ref6.update,
droppable = _ref6.droppable;
if (data.draggableId !== update.draggableId) {
return;
}
dispatch({
type: 'UPDATE_DRAG',
payload: {
update: update
}
});
if (update.mode === 'SNAP') {
/**
* Updating the position in a microtask to resolve timing issues.
*
* When doing cross-axis dragging, the drop indicator in the new
* droppable will mount and update in a `onPendingDragUpdate` too.
*
* The microtask ensures that the indicator will have updated by
* the time this runs, so the preview will have the correct
* location of the indicator.
*/
queueMicrotask(function () {
/**
* Because this update occurs in a microtask, we need to check
* that the drag is still happening.
*
* If it has ended we should not try to update the preview.
*/
var dragState = getDragState();
if (!dragState.isDragging) {
return;
}
/**
* The placeholder might not exist if its associated
* draggable unmounts in a virtual list.
*/
var placeholder = findPlaceholder(contextId);
var placeholderRect = placeholder ? placeholder.getBoundingClientRect() : null;
/**
* The drop indicator might not exist if the current target
* is null
*/
var dropIndicator = findDropIndicator();
var dropIndicatorRect = dropIndicator ? dropIndicator.getBoundingClientRect() : null;
dispatch({
type: 'UPDATE_KEYBOARD_PREVIEW',
payload: {
update: update,
draggableDimensions: draggableDimensions,
droppable: droppable,
placeholderRect: placeholderRect,
dropIndicatorRect: dropIndicatorRect
}
});
});
}
},
onBeforeDragEnd: function onBeforeDragEnd(_ref7) {
var draggableId = _ref7.draggableId;
if (draggableId !== data.draggableId) {
return;
}
rbdInvariant(isMountedRef.current, 'isMounted onBeforeDragEnd');
dispatch({
type: 'DROP'
});
}
}), monitorForElements({
canMonitor: function canMonitor(_ref8) {
var source = _ref8.source;
if (!isDraggableData(source.data)) {
// not dragging something from the migration layer
// we should not monitor it
return false;
}
return source.data.contextId === data.contextId && source.data.draggableId === data.draggableId;
},
onDrag: function onDrag(_ref9) {
var location = _ref9.location;
dispatch({
type: 'UPDATE_POINTER_PREVIEW',
payload: {
pointerLocation: location
}
});
}
}));
}, [data.draggableId, data.contextId, monitorForLifecycle, shouldRenderCloneWhileDragging, direction, contextId, draggableDimensions, getDragState]);
var provided = useMemo(function () {
return {
draggableProps: _defineProperty(_defineProperty(_defineProperty({}, attributes.draggable.contextId, contextId), attributes.draggable.id, draggableId), "style", getDraggableProvidedStyle({
draggableDimensions: draggableDimensions,
draggableState: state
})),
dragHandleProps: _defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty({
role: 'button',
'aria-describedby': getHiddenTextElementId(contextId)
}, attributes.dragHandle.contextId, contextId), attributes.dragHandle.draggableId, draggableId), "tabIndex", 0), "draggable", false), "onDragStart", noop),
innerRef: setElement
};
}, [contextId, draggableId, draggableDimensions, state, setElement]);
var snapshot = useDraggableStateSnapshot({
draggingOver: state.draggingOver,
isClone: false,
isDragging: isDragging,
mode: isDragging ? state.mode : null
});
var rubric = useMemo(function () {
return {
draggableId: draggableId,
type: type,
source: {
droppableId: droppableId,
index: index
}
};
}, [draggableId, droppableId, index, type]);
return /*#__PURE__*/React.createElement(React.Fragment, null, isHiding ? null : children(provided, snapshot, rubric), hasPlaceholder && /*#__PURE__*/React.createElement(Placeholder, {
ref: placeholderRef
}));
}