You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

178 lines
5.2 KiB

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> = (props) => {
// reference to DOM node
const ref = useRef<HTMLElement | null>(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;