781 lines
22 KiB
TypeScript
781 lines
22 KiB
TypeScript
/*
|
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
* or more contributor license agreements. Licensed under the Elastic License
|
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
* 2.0.
|
|
*/
|
|
|
|
import './drag_drop.scss';
|
|
import React, { useContext, useEffect, memo } from 'react';
|
|
import classNames from 'classnames';
|
|
import { keys, EuiScreenReaderOnly } from '@elastic/eui';
|
|
import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect';
|
|
import {
|
|
DragDropIdentifier,
|
|
DropIdentifier,
|
|
DragContext,
|
|
DragContextState,
|
|
nextValidDropTarget,
|
|
ReorderContext,
|
|
ReorderState,
|
|
DropHandler,
|
|
announce,
|
|
} from './providers';
|
|
import { trackUiEvent } from '../lens_ui_telemetry';
|
|
import { DropType } from '../types';
|
|
|
|
export type DroppableEvent = React.DragEvent<HTMLElement>;
|
|
|
|
/**
|
|
* The base props to the DragDrop component.
|
|
*/
|
|
interface BaseProps {
|
|
/**
|
|
* The CSS class(es) for the root element.
|
|
*/
|
|
className?: string;
|
|
|
|
/**
|
|
* The event handler that fires when an item
|
|
* is dropped onto this DragDrop component.
|
|
*/
|
|
onDrop?: DropHandler;
|
|
/**
|
|
* The value associated with this item.
|
|
*/
|
|
value: DragDropIdentifier;
|
|
|
|
/**
|
|
* Optional comparison function to check whether a value is the dragged one
|
|
*/
|
|
isValueEqual?: (value1: unknown, value2: unknown) => boolean;
|
|
|
|
/**
|
|
* The React element which will be passed the draggable handlers
|
|
*/
|
|
children: React.ReactElement;
|
|
/**
|
|
* Indicates whether or not this component is draggable.
|
|
*/
|
|
draggable?: boolean;
|
|
|
|
/**
|
|
* Additional class names to apply when another element is over the drop target
|
|
*/
|
|
getAdditionalClassesOnEnter?: (dropType?: DropType) => string | undefined;
|
|
/**
|
|
* Additional class names to apply when another element is droppable for a currently dragged item
|
|
*/
|
|
getAdditionalClassesOnDroppable?: (dropType?: DropType) => string | undefined;
|
|
|
|
/**
|
|
* The optional test subject associated with this DOM element.
|
|
*/
|
|
dataTestSubj?: string;
|
|
|
|
/**
|
|
* items belonging to the same group that can be reordered
|
|
*/
|
|
reorderableGroup?: Array<{ id: string }>;
|
|
|
|
/**
|
|
* Indicates to the user whether the currently dragged item
|
|
* will be moved or copied
|
|
*/
|
|
dragType?: 'copy' | 'move';
|
|
|
|
/**
|
|
* Indicates the type of a drop - when undefined, the currently dragged item
|
|
* cannot be dropped onto this component.
|
|
*/
|
|
dropType?: DropType;
|
|
/**
|
|
* Order for keyboard dragging. This takes an array of numbers which will be used to order hierarchically
|
|
*/
|
|
order: number[];
|
|
}
|
|
|
|
/**
|
|
* The props for a draggable instance of that component.
|
|
*/
|
|
interface DragInnerProps extends BaseProps {
|
|
setKeyboardMode: DragContextState['setKeyboardMode'];
|
|
setDragging: DragContextState['setDragging'];
|
|
setActiveDropTarget: DragContextState['setActiveDropTarget'];
|
|
setA11yMessage: DragContextState['setA11yMessage'];
|
|
activeDraggingProps?: {
|
|
keyboardMode: DragContextState['keyboardMode'];
|
|
activeDropTarget: DragContextState['activeDropTarget'];
|
|
dropTargetsByOrder: DragContextState['dropTargetsByOrder'];
|
|
};
|
|
onDragStart?: (
|
|
target?:
|
|
| DroppableEvent['currentTarget']
|
|
| React.KeyboardEvent<HTMLButtonElement>['currentTarget']
|
|
) => void;
|
|
onDragEnd?: () => void;
|
|
extraKeyboardHandler?: (e: React.KeyboardEvent<HTMLButtonElement>) => void;
|
|
ariaDescribedBy?: string;
|
|
}
|
|
|
|
/**
|
|
* The props for a non-draggable instance of that component.
|
|
*/
|
|
interface DropInnerProps extends BaseProps {
|
|
dragging: DragContextState['dragging'];
|
|
keyboardMode: DragContextState['keyboardMode'];
|
|
setKeyboardMode: DragContextState['setKeyboardMode'];
|
|
setDragging: DragContextState['setDragging'];
|
|
setActiveDropTarget: DragContextState['setActiveDropTarget'];
|
|
setA11yMessage: DragContextState['setA11yMessage'];
|
|
registerDropTarget: DragContextState['registerDropTarget'];
|
|
isActiveDropTarget: boolean;
|
|
isNotDroppable: boolean;
|
|
}
|
|
|
|
const lnsLayerPanelDimensionMargin = 8;
|
|
|
|
export const DragDrop = (props: BaseProps) => {
|
|
const {
|
|
dragging,
|
|
setDragging,
|
|
keyboardMode,
|
|
registerDropTarget,
|
|
dropTargetsByOrder,
|
|
setKeyboardMode,
|
|
activeDropTarget,
|
|
setActiveDropTarget,
|
|
setA11yMessage,
|
|
} = useContext(DragContext);
|
|
|
|
const { value, draggable, dropType, reorderableGroup } = props;
|
|
const isDragging = !!(draggable && value.id === dragging?.id);
|
|
|
|
const activeDraggingProps = isDragging
|
|
? {
|
|
keyboardMode,
|
|
activeDropTarget,
|
|
dropTargetsByOrder,
|
|
}
|
|
: undefined;
|
|
|
|
if (draggable && !dropType) {
|
|
const dragProps = {
|
|
...props,
|
|
activeDraggingProps,
|
|
setKeyboardMode,
|
|
setDragging,
|
|
setActiveDropTarget,
|
|
setA11yMessage,
|
|
};
|
|
if (reorderableGroup && reorderableGroup.length > 1) {
|
|
return <ReorderableDrag {...dragProps} reorderableGroup={reorderableGroup} />;
|
|
} else {
|
|
return <DragInner {...dragProps} />;
|
|
}
|
|
}
|
|
|
|
const isActiveDropTarget = Boolean(activeDropTarget?.id === value.id);
|
|
const dropProps = {
|
|
...props,
|
|
keyboardMode,
|
|
setKeyboardMode,
|
|
dragging,
|
|
setDragging,
|
|
isActiveDropTarget,
|
|
setActiveDropTarget,
|
|
registerDropTarget,
|
|
setA11yMessage,
|
|
isNotDroppable:
|
|
// If the configuration has provided a droppable flag, but this particular item is not
|
|
// droppable, then it should be less prominent. Ignores items that are both
|
|
// draggable and drop targets
|
|
!!(!dropType && dragging && value.id !== dragging.id),
|
|
};
|
|
if (
|
|
reorderableGroup &&
|
|
reorderableGroup.length > 1 &&
|
|
reorderableGroup?.some((i) => i.id === dragging?.id)
|
|
) {
|
|
return <ReorderableDrop {...dropProps} reorderableGroup={reorderableGroup} />;
|
|
}
|
|
return <DropInner {...dropProps} />;
|
|
};
|
|
|
|
const removeSelectionBeforeDragging = () => {
|
|
const selection = window.getSelection();
|
|
if (selection) {
|
|
selection.removeAllRanges();
|
|
}
|
|
};
|
|
|
|
const DragInner = memo(function DragInner({
|
|
dataTestSubj,
|
|
className,
|
|
value,
|
|
children,
|
|
setDragging,
|
|
setKeyboardMode,
|
|
setActiveDropTarget,
|
|
order,
|
|
activeDraggingProps,
|
|
dragType,
|
|
onDragStart,
|
|
onDragEnd,
|
|
extraKeyboardHandler,
|
|
ariaDescribedBy,
|
|
setA11yMessage,
|
|
}: DragInnerProps) {
|
|
const keyboardMode = activeDraggingProps?.keyboardMode;
|
|
const activeDropTarget = activeDraggingProps?.activeDropTarget;
|
|
const dropTargetsByOrder = activeDraggingProps?.dropTargetsByOrder;
|
|
|
|
const dragStart = (
|
|
e: DroppableEvent | React.KeyboardEvent<HTMLButtonElement>,
|
|
keyboardModeOn?: boolean
|
|
) => {
|
|
// Setting stopPropgagation causes Chrome failures, so
|
|
// we are manually checking if we've already handled this
|
|
// in a nested child, and doing nothing if so...
|
|
if (e && 'dataTransfer' in e && e.dataTransfer.getData('text')) {
|
|
return;
|
|
}
|
|
|
|
// We only can reach the dragStart method if the element is draggable,
|
|
// so we know we have DraggableProps if we reach this code.
|
|
if (e && 'dataTransfer' in e) {
|
|
e.dataTransfer.setData('text', value.humanData.label);
|
|
}
|
|
|
|
// Chrome causes issues if you try to render from within a
|
|
// dragStart event, so we drop a setTimeout to avoid that.
|
|
|
|
const currentTarget = e?.currentTarget;
|
|
|
|
setTimeout(() => {
|
|
setDragging({
|
|
...value,
|
|
ghost: keyboardModeOn
|
|
? {
|
|
children,
|
|
style: { width: currentTarget.offsetWidth, height: currentTarget.offsetHeight },
|
|
}
|
|
: undefined,
|
|
});
|
|
setA11yMessage(announce.lifted(value.humanData));
|
|
if (keyboardModeOn) {
|
|
setKeyboardMode(true);
|
|
}
|
|
if (onDragStart) {
|
|
onDragStart(currentTarget);
|
|
}
|
|
});
|
|
};
|
|
|
|
const dragEnd = (e?: DroppableEvent) => {
|
|
e?.stopPropagation();
|
|
setDragging(undefined);
|
|
setActiveDropTarget(undefined);
|
|
setKeyboardMode(false);
|
|
setA11yMessage(announce.cancelled(value.humanData));
|
|
if (onDragEnd) {
|
|
onDragEnd();
|
|
}
|
|
};
|
|
const dropToActiveDropTarget = () => {
|
|
if (activeDropTarget) {
|
|
trackUiEvent('drop_total');
|
|
const { dropType, humanData, onDrop: onTargetDrop } = activeDropTarget;
|
|
setTimeout(() => setA11yMessage(announce.dropped(value.humanData, humanData, dropType)));
|
|
onTargetDrop(value, dropType);
|
|
}
|
|
};
|
|
|
|
const setNextTarget = (reversed = false) => {
|
|
if (!order) {
|
|
return;
|
|
}
|
|
|
|
const nextTarget = nextValidDropTarget(
|
|
dropTargetsByOrder,
|
|
activeDropTarget,
|
|
[order.join(',')],
|
|
(el) => el?.dropType !== 'reorder',
|
|
reversed
|
|
);
|
|
|
|
setActiveDropTarget(nextTarget);
|
|
setA11yMessage(
|
|
nextTarget
|
|
? announce.selectedTarget(value.humanData, nextTarget?.humanData, nextTarget?.dropType)
|
|
: announce.noTarget()
|
|
);
|
|
};
|
|
const shouldShowGhostImageInstead =
|
|
dragType === 'move' &&
|
|
keyboardMode &&
|
|
activeDropTarget &&
|
|
activeDropTarget.dropType !== 'reorder';
|
|
return (
|
|
<div
|
|
className={classNames(className, {
|
|
'lnsDragDrop-isHidden-noFocus': shouldShowGhostImageInstead,
|
|
})}
|
|
data-test-subj={`lnsDragDrop_draggable-${value.humanData.label}`}
|
|
>
|
|
<EuiScreenReaderOnly showOnFocus>
|
|
<button
|
|
aria-label={value.humanData.label}
|
|
aria-describedby={ariaDescribedBy || `lnsDragDrop-keyboardInstructions`}
|
|
className="lnsDragDrop__keyboardHandler"
|
|
data-test-subj="lnsDragDrop-keyboardHandler"
|
|
onBlur={() => {
|
|
if (activeDraggingProps) {
|
|
dragEnd();
|
|
}
|
|
}}
|
|
onKeyDown={(e: React.KeyboardEvent<HTMLButtonElement>) => {
|
|
const { key } = e;
|
|
if (key === keys.ENTER || key === keys.SPACE) {
|
|
if (activeDropTarget) {
|
|
dropToActiveDropTarget();
|
|
}
|
|
|
|
if (activeDraggingProps) {
|
|
dragEnd();
|
|
} else {
|
|
dragStart(e, true);
|
|
}
|
|
} else if (key === keys.ESCAPE) {
|
|
if (activeDraggingProps) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
dragEnd();
|
|
}
|
|
}
|
|
if (extraKeyboardHandler) {
|
|
extraKeyboardHandler(e);
|
|
}
|
|
if (keyboardMode && (keys.ARROW_LEFT === key || keys.ARROW_RIGHT === key)) {
|
|
setNextTarget(!!(keys.ARROW_LEFT === key));
|
|
}
|
|
}}
|
|
/>
|
|
</EuiScreenReaderOnly>
|
|
|
|
{React.cloneElement(children, {
|
|
'data-test-subj': dataTestSubj || 'lnsDragDrop',
|
|
className: classNames(children.props.className, 'lnsDragDrop', 'lnsDragDrop-isDraggable', {
|
|
'lnsDragDrop-isHidden':
|
|
(activeDraggingProps && dragType === 'move' && !keyboardMode) ||
|
|
shouldShowGhostImageInstead,
|
|
}),
|
|
draggable: true,
|
|
onDragEnd: dragEnd,
|
|
onDragStart: dragStart,
|
|
onMouseDown: removeSelectionBeforeDragging,
|
|
})}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
const DropInner = memo(function DropInner(props: DropInnerProps) {
|
|
const {
|
|
dataTestSubj,
|
|
className,
|
|
onDrop,
|
|
value,
|
|
children,
|
|
draggable,
|
|
dragging,
|
|
isNotDroppable,
|
|
dropType,
|
|
order,
|
|
getAdditionalClassesOnEnter,
|
|
getAdditionalClassesOnDroppable,
|
|
isActiveDropTarget,
|
|
registerDropTarget,
|
|
setActiveDropTarget,
|
|
keyboardMode,
|
|
setKeyboardMode,
|
|
setDragging,
|
|
setA11yMessage,
|
|
} = props;
|
|
|
|
useShallowCompareEffect(() => {
|
|
if (dropType && onDrop && keyboardMode) {
|
|
registerDropTarget(order, { ...value, onDrop, dropType });
|
|
return () => {
|
|
registerDropTarget(order, undefined);
|
|
};
|
|
}
|
|
}, [order, value, registerDropTarget, dropType, keyboardMode]);
|
|
|
|
const classesOnEnter = getAdditionalClassesOnEnter?.(dropType);
|
|
const classesOnDroppable = getAdditionalClassesOnDroppable?.(dropType);
|
|
|
|
const classes = classNames(
|
|
'lnsDragDrop',
|
|
{
|
|
'lnsDragDrop-isDraggable': draggable,
|
|
'lnsDragDrop-isDroppable': !draggable,
|
|
'lnsDragDrop-isDropTarget': dropType && dropType !== 'reorder',
|
|
'lnsDragDrop-isActiveDropTarget': dropType && isActiveDropTarget && dropType !== 'reorder',
|
|
'lnsDragDrop-isNotDroppable': isNotDroppable,
|
|
},
|
|
classesOnEnter && { [classesOnEnter]: isActiveDropTarget },
|
|
classesOnDroppable && { [classesOnDroppable]: dropType }
|
|
);
|
|
|
|
const dragOver = (e: DroppableEvent) => {
|
|
if (!dropType) {
|
|
return;
|
|
}
|
|
e.preventDefault();
|
|
|
|
// An optimization to prevent a bunch of React churn.
|
|
if (!isActiveDropTarget && dragging && onDrop) {
|
|
setActiveDropTarget({ ...value, dropType, onDrop });
|
|
setA11yMessage(announce.selectedTarget(dragging.humanData, value.humanData, dropType));
|
|
}
|
|
};
|
|
|
|
const dragLeave = () => {
|
|
setActiveDropTarget(undefined);
|
|
};
|
|
|
|
const drop = (e: DroppableEvent | React.KeyboardEvent<HTMLButtonElement>) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
if (onDrop && dropType && dragging) {
|
|
trackUiEvent('drop_total');
|
|
onDrop(dragging, dropType);
|
|
setTimeout(() =>
|
|
setA11yMessage(announce.dropped(dragging.humanData, value.humanData, dropType))
|
|
);
|
|
}
|
|
setDragging(undefined);
|
|
setActiveDropTarget(undefined);
|
|
setKeyboardMode(false);
|
|
};
|
|
|
|
const ghost =
|
|
isActiveDropTarget && dropType !== 'reorder' && dragging?.ghost ? dragging.ghost : undefined;
|
|
|
|
return (
|
|
<>
|
|
{React.cloneElement(children, {
|
|
'data-test-subj': dataTestSubj || 'lnsDragDrop',
|
|
className: classNames(children.props.className, classes, className),
|
|
onDragOver: dragOver,
|
|
onDragLeave: dragLeave,
|
|
onDrop: drop,
|
|
draggable,
|
|
})}
|
|
{ghost
|
|
? React.cloneElement(ghost.children, {
|
|
className: classNames(ghost.children.props.className, 'lnsDragDrop_ghost'),
|
|
style: ghost.style,
|
|
})
|
|
: null}
|
|
</>
|
|
);
|
|
});
|
|
|
|
const ReorderableDrag = memo(function ReorderableDrag(
|
|
props: DragInnerProps & { reorderableGroup: Array<{ id: string }>; dragging?: DragDropIdentifier }
|
|
) {
|
|
const {
|
|
reorderState: { isReorderOn, reorderedItems, direction },
|
|
setReorderState,
|
|
} = useContext(ReorderContext);
|
|
|
|
const {
|
|
value,
|
|
setActiveDropTarget,
|
|
activeDraggingProps,
|
|
reorderableGroup,
|
|
setA11yMessage,
|
|
} = props;
|
|
|
|
const keyboardMode = activeDraggingProps?.keyboardMode;
|
|
const activeDropTarget = activeDraggingProps?.activeDropTarget;
|
|
const dropTargetsByOrder = activeDraggingProps?.dropTargetsByOrder;
|
|
const isDragging = !!activeDraggingProps;
|
|
|
|
const isFocusInGroup = keyboardMode
|
|
? isDragging &&
|
|
(!activeDropTarget || reorderableGroup.some((i) => i.id === activeDropTarget?.id))
|
|
: isDragging;
|
|
|
|
useEffect(() => {
|
|
setReorderState((s: ReorderState) => ({
|
|
...s,
|
|
isReorderOn: isFocusInGroup,
|
|
}));
|
|
}, [setReorderState, isFocusInGroup]);
|
|
|
|
const onReorderableDragStart = (
|
|
currentTarget?:
|
|
| DroppableEvent['currentTarget']
|
|
| React.KeyboardEvent<HTMLButtonElement>['currentTarget']
|
|
) => {
|
|
if (currentTarget) {
|
|
const height = currentTarget.offsetHeight + lnsLayerPanelDimensionMargin;
|
|
setReorderState((s: ReorderState) => ({
|
|
...s,
|
|
draggingHeight: height,
|
|
}));
|
|
}
|
|
};
|
|
|
|
const onReorderableDragEnd = () => {
|
|
resetReorderState();
|
|
};
|
|
|
|
const resetReorderState = () =>
|
|
setReorderState((s: ReorderState) => ({
|
|
...s,
|
|
reorderedItems: [],
|
|
}));
|
|
|
|
const extraKeyboardHandler = (e: React.KeyboardEvent<HTMLButtonElement>) => {
|
|
if (isReorderOn && keyboardMode) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
let activeDropTargetIndex = reorderableGroup.findIndex((i) => i.id === value.id);
|
|
if (activeDropTarget) {
|
|
const index = reorderableGroup.findIndex((i) => i.id === activeDropTarget?.id);
|
|
if (index !== -1) activeDropTargetIndex = index;
|
|
}
|
|
if (e.key === keys.ARROW_LEFT || e.key === keys.ARROW_RIGHT) {
|
|
resetReorderState();
|
|
setActiveDropTarget(undefined);
|
|
} else if (keys.ARROW_DOWN === e.key) {
|
|
if (activeDropTargetIndex < reorderableGroup.length - 1) {
|
|
const nextTarget = nextValidDropTarget(
|
|
dropTargetsByOrder,
|
|
activeDropTarget,
|
|
[props.order.join(',')],
|
|
(el) => el?.dropType === 'reorder'
|
|
);
|
|
onReorderableDragOver(nextTarget);
|
|
}
|
|
} else if (keys.ARROW_UP === e.key) {
|
|
if (activeDropTargetIndex > 0) {
|
|
const nextTarget = nextValidDropTarget(
|
|
dropTargetsByOrder,
|
|
activeDropTarget,
|
|
[props.order.join(',')],
|
|
(el) => el?.dropType === 'reorder',
|
|
true
|
|
);
|
|
onReorderableDragOver(nextTarget);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const onReorderableDragOver = (target?: DropIdentifier) => {
|
|
if (!target) {
|
|
setReorderState((s: ReorderState) => ({
|
|
...s,
|
|
reorderedItems: [],
|
|
}));
|
|
setA11yMessage(announce.selectedTarget(value.humanData, value.humanData, 'reorder'));
|
|
setActiveDropTarget(target);
|
|
return;
|
|
}
|
|
const droppingIndex = reorderableGroup.findIndex((i) => i.id === target.id);
|
|
const draggingIndex = reorderableGroup.findIndex((i) => i.id === value?.id);
|
|
if (draggingIndex === -1) {
|
|
return;
|
|
}
|
|
setActiveDropTarget(target);
|
|
|
|
setA11yMessage(announce.selectedTarget(value.humanData, target.humanData, 'reorder'));
|
|
|
|
setReorderState((s: ReorderState) =>
|
|
draggingIndex < droppingIndex
|
|
? {
|
|
...s,
|
|
reorderedItems: reorderableGroup.slice(draggingIndex + 1, droppingIndex + 1),
|
|
direction: '-',
|
|
}
|
|
: {
|
|
...s,
|
|
reorderedItems: reorderableGroup.slice(droppingIndex, draggingIndex),
|
|
direction: '+',
|
|
}
|
|
);
|
|
};
|
|
|
|
const areItemsReordered = isDragging && keyboardMode && reorderedItems.length;
|
|
|
|
return (
|
|
<div
|
|
data-test-subj="lnsDragDrop-reorderableDrag"
|
|
className={
|
|
isDragging
|
|
? 'lnsDragDrop-reorderable lnsDragDrop-translatableDrag'
|
|
: 'lnsDragDrop-reorderable'
|
|
}
|
|
style={
|
|
areItemsReordered
|
|
? {
|
|
transform: `translateY(${direction === '+' ? '-' : '+'}${reorderedItems.reduce(
|
|
(acc, cur) => acc + Number(cur.height || 0) + lnsLayerPanelDimensionMargin,
|
|
0
|
|
)}px)`,
|
|
}
|
|
: undefined
|
|
}
|
|
>
|
|
<DragInner
|
|
{...props}
|
|
ariaDescribedBy="lnsDragDrop-keyboardInstructionsWithReorder"
|
|
extraKeyboardHandler={extraKeyboardHandler}
|
|
onDragStart={onReorderableDragStart}
|
|
onDragEnd={onReorderableDragEnd}
|
|
/>
|
|
</div>
|
|
);
|
|
});
|
|
|
|
const ReorderableDrop = memo(function ReorderableDrop(
|
|
props: DropInnerProps & { reorderableGroup: Array<{ id: string }> }
|
|
) {
|
|
const {
|
|
onDrop,
|
|
value,
|
|
dragging,
|
|
setDragging,
|
|
setKeyboardMode,
|
|
isActiveDropTarget,
|
|
setActiveDropTarget,
|
|
reorderableGroup,
|
|
setA11yMessage,
|
|
dropType,
|
|
} = props;
|
|
|
|
const currentIndex = reorderableGroup.findIndex((i) => i.id === value.id);
|
|
|
|
const {
|
|
reorderState: { isReorderOn, reorderedItems, draggingHeight, direction },
|
|
setReorderState,
|
|
} = useContext(ReorderContext);
|
|
|
|
const heightRef = React.useRef<HTMLDivElement>(null);
|
|
|
|
const isReordered =
|
|
isReorderOn && reorderedItems.some((el) => el.id === value.id) && reorderedItems.length;
|
|
|
|
useEffect(() => {
|
|
if (isReordered && heightRef.current?.clientHeight) {
|
|
setReorderState((s) => ({
|
|
...s,
|
|
reorderedItems: s.reorderedItems.map((el) =>
|
|
el.id === value.id
|
|
? {
|
|
...el,
|
|
height: heightRef.current?.clientHeight,
|
|
}
|
|
: el
|
|
),
|
|
}));
|
|
}
|
|
}, [isReordered, setReorderState, value.id]);
|
|
|
|
const onReorderableDragOver = (e: DroppableEvent) => {
|
|
if (!dropType) {
|
|
return;
|
|
}
|
|
e.preventDefault();
|
|
|
|
// An optimization to prevent a bunch of React churn.
|
|
if (!isActiveDropTarget && dropType && onDrop) {
|
|
setActiveDropTarget({ ...value, dropType, onDrop });
|
|
}
|
|
|
|
const draggingIndex = reorderableGroup.findIndex((i) => i.id === dragging?.id);
|
|
|
|
if (!dragging || draggingIndex === -1) {
|
|
return;
|
|
}
|
|
const droppingIndex = currentIndex;
|
|
if (draggingIndex === droppingIndex) {
|
|
setReorderState((s: ReorderState) => ({
|
|
...s,
|
|
reorderedItems: [],
|
|
}));
|
|
}
|
|
|
|
setReorderState((s: ReorderState) =>
|
|
draggingIndex < droppingIndex
|
|
? {
|
|
...s,
|
|
reorderedItems: reorderableGroup.slice(draggingIndex + 1, droppingIndex + 1),
|
|
direction: '-',
|
|
}
|
|
: {
|
|
...s,
|
|
reorderedItems: reorderableGroup.slice(droppingIndex, draggingIndex),
|
|
direction: '+',
|
|
}
|
|
);
|
|
};
|
|
|
|
const onReorderableDrop = (e: DroppableEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
setActiveDropTarget(undefined);
|
|
setDragging(undefined);
|
|
setKeyboardMode(false);
|
|
|
|
if (onDrop && dropType && dragging) {
|
|
trackUiEvent('drop_total');
|
|
onDrop(dragging, 'reorder');
|
|
// setTimeout ensures it will run after dragEnd messaging
|
|
setTimeout(() =>
|
|
setA11yMessage(announce.dropped(dragging.humanData, value.humanData, 'reorder'))
|
|
);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div
|
|
style={
|
|
reorderedItems.some((i) => i.id === value.id)
|
|
? {
|
|
transform: `translateY(${direction}${draggingHeight}px)`,
|
|
}
|
|
: undefined
|
|
}
|
|
ref={heightRef}
|
|
data-test-subj="lnsDragDrop-translatableDrop"
|
|
className="lnsDragDrop-translatableDrop lnsDragDrop-reorderable"
|
|
>
|
|
<DropInner {...props} />
|
|
</div>
|
|
|
|
<div
|
|
data-test-subj="lnsDragDrop-reorderableDropLayer"
|
|
className={classNames('lnsDragDrop', {
|
|
['lnsDragDrop__reorderableDrop']: dragging && dropType,
|
|
})}
|
|
onDrop={onReorderableDrop}
|
|
onDragOver={onReorderableDragOver}
|
|
onDragLeave={() => {
|
|
setReorderState((s: ReorderState) => ({
|
|
...s,
|
|
reorderedItems: [],
|
|
}));
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
});
|