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

462 lines (450 loc) 17.2 kB
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 })); }