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

340 lines (333 loc) 9.77 kB
import { useCallback } from 'react'; import { bindAll } from 'bind-event-listener'; // eslint-disable-next-line import/no-extraneous-dependencies import { attributes, customAttributes, getAttribute } from '../../utils/attributes'; import { findClosestScrollContainer } from '../../utils/find-closest-scroll-container'; import { getElement } from '../../utils/find-element'; import { getBestCrossAxisDroppable } from '../../utils/get-best-cross-axis-droppable'; import { getElementByDraggableLocation } from '../../utils/get-element-by-draggable-location'; import { isSameLocation } from '../draggable-location'; import { getActualDestination } from '../get-destination'; import { rbdInvariant } from '../rbd-invariant'; /** * Finds the element's scroll container and scrolls it to the top. * * This is used for cross-axis drags to provide a consistent starting point * in the list. * * The behavior differs to `react-beautiful-dnd` which would find the * index closest to the current visual position. That was not preserved for * performance cost reasons. */ function scrollToTop(element) { const scrollContainer = findClosestScrollContainer(element); if (!scrollContainer) { return; } scrollContainer.scrollTo(0, 0); } const moveHandlers = { mainAxis: { prev(event, { dragController }) { /** * Preventing default to stop scrolling caused by arrow key press. */ event.preventDefault(); const dragState = dragController.getDragState(); rbdInvariant(dragState.isDragging); const { sourceLocation, targetLocation } = dragState; if (!targetLocation) { return; } if (targetLocation.index === 0) { return; } const nextLocation = { ...targetLocation, index: targetLocation.index - 1 }; const nextDestination = getActualDestination({ start: sourceLocation, target: nextLocation }); /** * There are two target indexes that correspond to a drop in the source location: * * 1. source.index (before the source, but after previous item) * 2. source.index + 1 (after the source, but before next item) * * We decrement by 2 when going over the source location, * so the user only perceives one of these indexes. */ if (isSameLocation(sourceLocation, nextDestination)) { nextLocation.index = targetLocation.index - 2; } dragController.updateDrag({ targetLocation: nextLocation }); }, next(event, { dragController, contextId }) { /** * Preventing default to stop scrolling caused by arrow key press. */ event.preventDefault(); const dragState = dragController.getDragState(); rbdInvariant(dragState.isDragging); const { sourceLocation, targetLocation } = dragState; if (!targetLocation) { return; } /** * Checks if we can move to the next position. * * Reasoning: if there is already a draggable with the current index, * then it is a possible target. */ const element = getElementByDraggableLocation(contextId, targetLocation); /** * This check is for virtual lists, and is a special case. * * When dragging, the element will unmount and we won't be able to find * the element for that index. * * This means for virtual lists, the normal check (element check) will fail. */ const isSame = isSameLocation(sourceLocation, targetLocation); if (!isSame && !element) { return; } const nextLocation = { ...targetLocation, index: targetLocation.index + 1 }; const nextDestination = getActualDestination({ start: sourceLocation, target: nextLocation }); /** * There are two target indexes that correspond to a drop in the source location: * * 1. source.index (before the source, but after previous item) * 2. source.index + 1 (after the source, but before next item) * * We increment by 2 when going over the source location, * so the user only perceives one of these indexes. */ if (isSameLocation(sourceLocation, nextDestination)) { nextLocation.index = targetLocation.index + 2; } dragController.updateDrag({ targetLocation: nextLocation }); } }, crossAxis: { prev(event, { dragController, droppableRegistry, contextId }) { /** * Preventing default to stop scrolling caused by arrow key press. */ event.preventDefault(); const dragState = dragController.getDragState(); rbdInvariant(dragState.isDragging); const { targetLocation, type } = dragState; if (!targetLocation) { return; } const before = getBestCrossAxisDroppable({ droppableId: targetLocation.droppableId, type, isMovingForward: false, contextId, droppableRegistry }); if (!before) { return; } scrollToTop(before); const nextLocation = { droppableId: getAttribute(before, attributes.droppable.id), index: 0 }; dragController.updateDrag({ targetLocation: nextLocation }); }, next(event, { dragController, droppableRegistry, contextId }) { /** * Preventing default to stop scrolling caused by arrow key press. */ event.preventDefault(); const dragState = dragController.getDragState(); rbdInvariant(dragState.isDragging); const { targetLocation, type } = dragState; if (!targetLocation) { return; } const after = getBestCrossAxisDroppable({ droppableId: targetLocation.droppableId, type, isMovingForward: true, contextId, droppableRegistry }); if (!after) { return; } scrollToTop(after); const nextLocation = { droppableId: getAttribute(after, attributes.droppable.id), index: 0 }; dragController.updateDrag({ targetLocation: nextLocation }); } } }; function preventDefault(event) { event.preventDefault(); } /** * These keys mostly have their default behavior prevented to stop scrolling. * * The tab key is prevented to lock focus in place. */ const commonKeyHandlers = { PageUp: preventDefault, PageDown: preventDefault, Home: preventDefault, End: preventDefault, Enter: preventDefault, Tab: preventDefault }; /** * Maps actions to keys. */ const keyHandlers = { vertical: { ...commonKeyHandlers, ArrowUp: moveHandlers.mainAxis.prev, ArrowDown: moveHandlers.mainAxis.next, ArrowLeft: moveHandlers.crossAxis.prev, ArrowRight: moveHandlers.crossAxis.next }, horizontal: { ...commonKeyHandlers, ArrowUp: moveHandlers.crossAxis.prev, ArrowDown: moveHandlers.crossAxis.next, ArrowLeft: moveHandlers.mainAxis.prev, ArrowRight: moveHandlers.mainAxis.next } }; export function useKeyboardControls({ dragController, droppableRegistry, contextId, setKeyboardCleanupFn }) { const startKeyboardDrag = useCallback(({ event: startEvent, draggableId, type, getSourceLocation, sourceElement }) => { dragController.startDrag({ draggableId, type, getSourceLocation, sourceElement, mode: 'SNAP' }); const sourceLocation = getSourceLocation(); const droppable = getElement({ attribute: attributes.droppable.id, value: sourceLocation.droppableId }); const direction = getAttribute(droppable, customAttributes.droppable.direction); rbdInvariant(direction === 'vertical' || direction === 'horizontal'); function cancelDrag() { dragController.stopDrag({ reason: 'CANCEL' }); } /** * All of these events should cancel the drag. * * These events were taken from `react-beautiful-dnd`. */ const cancelBindings = ['mousedown', 'mouseup', 'click', 'touchstart', 'resize', 'wheel', 'visibilitychange'].map(type => { return { type, listener: cancelDrag }; }); const cleanupFn = bindAll(window, [{ type: 'keydown', listener(event) { var _keyHandlers$directio, _keyHandlers$directio2; /** * Ignores the keydown event which triggered the drag start, * so it doesn't trigger an immediate drop. */ if (event === startEvent) { return; } const { isDragging } = dragController.getDragState(); if (!isDragging) { return; } if (event.key === ' ') { event.preventDefault(); dragController.stopDrag({ reason: 'DROP' }); return; } if (event.key === 'Escape') { event.preventDefault(); dragController.stopDrag({ reason: 'CANCEL' }); return; } (_keyHandlers$directio = (_keyHandlers$directio2 = keyHandlers[direction])[event.key]) === null || _keyHandlers$directio === void 0 ? void 0 : _keyHandlers$directio.call(_keyHandlers$directio2, event, { dragController, droppableRegistry, contextId }); } }, ...cancelBindings]); setKeyboardCleanupFn(cleanupFn); }, [contextId, dragController, droppableRegistry, setKeyboardCleanupFn]); return { startKeyboardDrag }; }