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
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;
|