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

257 lines (252 loc) 8.51 kB
import React, { useCallback, useEffect, useMemo, useReducer, useRef } from 'react'; // eslint-disable-next-line import/no-extraneous-dependencies import { createPortal } from 'react-dom'; import invariant from 'tiny-invariant'; import { attachClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge'; import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'; import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; import { useDragDropContext } from '../drag-drop-context/internal-context'; import { useMonitorForLifecycle } from '../drag-drop-context/lifecycle-context'; import { isDraggableData } from '../draggable/data'; import { useLayoutEffect } from '../hooks/use-isomorphic-layout-effect'; import { attributes, customAttributes, setAttributes } from '../utils/attributes'; import { useStable } from '../utils/use-stable'; import { useDroppableData } from './data'; import { DraggableClone } from './draggable-clone'; import { DropIndicator } from './drop-indicator'; import { DroppableContextProvider, useParentDroppableId } from './droppable-context'; import { idleState, reducer } from './state'; import { VirtualPlaceholder } from './virtual-placeholder'; export function Droppable({ children, droppableId, type = 'DEFAULT', // This default value replicates `react-beautiful-dnd`, direction = 'vertical', mode = 'standard', renderClone, getContainerForClone, isDropDisabled = false }) { const getIsDropDisabled = useStable(isDropDisabled); const { contextId, droppableRegistry } = useDragDropContext(); const data = useDroppableData({ contextId, droppableId, getIsDropDisabled }); const elementRef = useRef(null); const setElement = useCallback(element => { if (element) { setAttributes(element, { [customAttributes.droppable.type]: type, [customAttributes.droppable.direction]: direction, /** * We set this manually instead of relying on the provided prop, * because for virtual lists this can be difficult to apply. * * `react-beautiful-dnd` does not actually break if this is not applied. */ [attributes.droppable.id]: droppableId, [attributes.droppable.contextId]: contextId }); } elementRef.current = element; }, [contextId, direction, droppableId, type]); const [state, dispatch] = useReducer(reducer, idleState); const { draggingFromThisWith, draggingOverWith, isDraggingOver } = state; const parentDroppableId = useParentDroppableId(); useEffect(() => { const element = elementRef.current; invariant(element instanceof HTMLElement, 'innerRef must provide an `HTMLElement`'); return combine(droppableRegistry.register({ droppableId, type, isDropDisabled, parentDroppableId, element, direction, mode }), dropTargetForElements({ element, getData({ input }) { return attachClosestEdge(data, { element, input, allowedEdges: direction === 'vertical' ? ['top', 'bottom'] : ['left', 'right'] }); }, canDrop({ source }) { if (!isDraggableData(source.data)) { // not dragging something from the migration layer // we should not allow dropping return false; } if (isDropDisabled) { return false; } return source.data.contextId === contextId && source.data.type === type; }, onDragLeave() { dispatch({ type: 'DRAG_CLEAR' }); } })); }, [data, droppableId, contextId, isDropDisabled, type, droppableRegistry, parentDroppableId, direction, mode]); const monitorForLifecycle = useMonitorForLifecycle(); useEffect(() => { function isEventRelevant(data) { var _data$destination; /** * If the draggable is of a different type to this droppable, * then we can ignore it. */ const isSameType = data.type === type; const isOverAfterUpdate = ((_data$destination = data.destination) === null || _data$destination === void 0 ? void 0 : _data$destination.droppableId) === droppableId; const isDragEnter = !isDraggingOver && isOverAfterUpdate; const isDragLeave = isDraggingOver && !isOverAfterUpdate; /** * A droppable will only have a meaningful state update if the user is entering or exiting it. */ const isDragEnterOrLeave = isDragEnter || isDragLeave; return isSameType && isDragEnterOrLeave; } return monitorForLifecycle({ onPendingDragStart({ start }) { if (!isEventRelevant({ destination: start.source, type: start.type })) { return; } dispatch({ type: 'DRAG_START', payload: { droppableId, start } }); }, onPendingDragUpdate({ update }) { if (!isEventRelevant(update)) { return; } dispatch({ type: 'DRAG_UPDATE', payload: { droppableId, update } }); }, onBeforeDragEnd() { /** * This is safe to call optimistically as it uses a stable idle state. * * If the droppable is already idle, it will not rerender. */ dispatch({ type: 'DRAG_CLEAR' }); } }); }, [droppableId, isDraggingOver, monitorForLifecycle, type]); const dropIndicator = useMemo(() => { if (!isDraggingOver) { return null; } return /*#__PURE__*/React.createElement(DropIndicator, { direction: direction, mode: mode }); }, [direction, isDraggingOver, mode]); const provided = useMemo(() => ({ innerRef: setElement, droppableProps: { [attributes.droppable.contextId]: contextId, [attributes.droppable.id]: droppableId }, /** * We only provide a drop indicator as the placeholder for * non-virtual lists. Otherwise it is portalled in. */ placeholder: mode === 'standard' ? dropIndicator : null }), [contextId, dropIndicator, droppableId, mode, setElement]); const snapshot = useMemo(() => ({ draggingFromThisWith, draggingOverWith, isDraggingOver, isUsingPlaceholder: isDraggingOver }), [draggingFromThisWith, draggingOverWith, isDraggingOver]); const element = elementRef.current; const shouldPortalDropIndicator = isDraggingOver && mode === 'virtual' && element; /** * Assumes that the ref points to the scroll container. */ useLayoutEffect(() => { if (!shouldPortalDropIndicator) { return; } const { position } = window.getComputedStyle(element); if (position !== 'static') { return; } const prevStyle = element.style.position; element.style.position = 'relative'; return () => { element.style.position = prevStyle; }; }, [element, shouldPortalDropIndicator]); /** * Used to disable the dragging style for the real draggable. */ const shouldRenderCloneWhileDragging = Boolean(renderClone); const contextValue = useMemo(() => { return { direction, droppableId, shouldRenderCloneWhileDragging, isDropDisabled, type, mode }; }, [direction, droppableId, shouldRenderCloneWhileDragging, isDropDisabled, type, mode]); /** * For virtual lists we portal a placeholder in when dragging from the list. * * This is because `<Draggable />`'s can be unmounted at any time, so we * cannot rely on rendering the placeholder as a sibling. */ const shouldPortalPlaceholder = draggingFromThisWith && mode === 'virtual' && element; return /*#__PURE__*/React.createElement(DroppableContextProvider, { value: contextValue }, children(provided, snapshot), shouldPortalDropIndicator && /*#__PURE__*/createPortal(dropIndicator, element), shouldPortalPlaceholder && /*#__PURE__*/createPortal( /*#__PURE__*/React.createElement(VirtualPlaceholder, { droppableId: droppableId, draggableId: draggingFromThisWith, type: type, direction: direction, isDropDisabled: isDropDisabled }), element), renderClone && /*#__PURE__*/React.createElement(DraggableClone, { droppableId: droppableId, type: type, getContainerForClone: getContainerForClone }, renderClone)); }