import React, { useRef, DragEvent, TransitionEvent } from 'react'; import { flushSync } from 'react-dom'; import { useMemo, useCallback } from 'use-memo-one'; import type { DraggableRubric, DraggableDescriptor } from '../../types'; import getStyle from './get-style'; import useDraggablePublisher from '../use-draggable-publisher/use-draggable-publisher'; import type { Args as PublisherArgs } from '../use-draggable-publisher/use-draggable-publisher'; import AppContext from '../context/app-context'; import DroppableContext from '../context/droppable-context'; import type { Props, DraggableProvided, DraggableStyle, DraggableProvidedDragHandleProps, } from './draggable-types'; import { useValidation, useClonePropValidation } from './use-validation'; import useRequiredContext from '../use-required-context'; function preventHtml5Dnd(event: DragEvent) { event.preventDefault(); } const Draggable: React.FunctionComponent = (props) => { // reference to DOM node const ref = useRef(null); const setRef = useCallback((el: HTMLElement | null = null) => { ref.current = el; }, []); const getRef = useCallback((): HTMLElement | null => ref.current, []); // context const { contextId, dragHandleUsageInstructionsId, registry } = useRequiredContext(AppContext); const { type, droppableId } = useRequiredContext(DroppableContext); const descriptor: DraggableDescriptor = useMemo( () => ({ id: props.draggableId, index: props.index, type, droppableId, }), [props.draggableId, props.index, type, droppableId], ); // props const { // ownProps children, draggableId, isEnabled, shouldRespectForcePress, canDragInteractiveElements, isClone, // mapProps mapped, // dispatchProps dropAnimationFinished: dropAnimationFinishedAction, } = props; // Validating props and innerRef useValidation(props, contextId, getRef); // Clones do not speak to the dimension marshal // We are violating the rules of hooks here: conditional hooks. // In this specific use case it is okay as an item will always either be a // clone or not for it's whole lifecycle /* eslint-disable react-hooks/rules-of-hooks */ // Being super sure that isClone is not changing during a draggable lifecycle useClonePropValidation(isClone); if (!isClone) { const forPublisher: PublisherArgs = useMemo( () => ({ descriptor, registry, getDraggableRef: getRef, canDragInteractiveElements, shouldRespectForcePress, isEnabled, }), [ descriptor, registry, getRef, canDragInteractiveElements, shouldRespectForcePress, isEnabled, ], ); useDraggablePublisher(forPublisher); } /* eslint-enable react-hooks/rules-of-hooks */ const dragHandleProps: DraggableProvidedDragHandleProps | null = useMemo( () => isEnabled ? { // See `draggable-types` for an explanation of why these are used tabIndex: 0, role: 'button', 'aria-describedby': dragHandleUsageInstructionsId, 'data-rfd-drag-handle-draggable-id': draggableId, 'data-rfd-drag-handle-context-id': contextId, draggable: false, onDragStart: preventHtml5Dnd, } : null, [contextId, dragHandleUsageInstructionsId, draggableId, isEnabled], ); const onMoveEnd = useCallback( (event: TransitionEvent) => { if (mapped.type !== 'DRAGGING') { return; } if (!mapped.dropping) { return; } // There might be other properties on the element that are // being transitioned. We do not want those to end a drop animation! if (event.propertyName !== 'transform') { return; } if (React.version.startsWith('16') || React.version.startsWith('17')) { // we can directly invoke the following method // because prior to react 18 state are not batched dropAnimationFinishedAction(); } else { // we must prevent automatic batching when using // react 18 and above by calling flushSync flushSync(dropAnimationFinishedAction); } }, [dropAnimationFinishedAction, mapped], ); const provided: DraggableProvided = useMemo(() => { const style: DraggableStyle = getStyle(mapped); const onTransitionEnd = mapped.type === 'DRAGGING' && mapped.dropping ? onMoveEnd : undefined; const result: DraggableProvided = { innerRef: setRef, draggableProps: { 'data-rfd-draggable-context-id': contextId, 'data-rfd-draggable-id': draggableId, style, onTransitionEnd, }, dragHandleProps, }; return result; }, [contextId, dragHandleProps, draggableId, mapped, onMoveEnd, setRef]); const rubric: DraggableRubric = useMemo( () => ({ draggableId: descriptor.id, type: descriptor.type, source: { index: descriptor.index, droppableId: descriptor.droppableId, }, }), [descriptor.droppableId, descriptor.id, descriptor.index, descriptor.type], ); return <>{children(provided, mapped.snapshot, rubric)}; }; export default Draggable;