mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[kbn-dnd-package] Divide draggable and droppable (#177282)
## Summary Refactors a `<DragDrop/>` component to `<Draggable/>` and `<Droppable/>`. 1. Performance gains Performance improvements – one rerender less and then longest render dropped by 30-40% (for 6x slowdown from 250 to 150ms action). The main reason is that the components that were switching between being Draggable and Droppable don't have to do it anymore so we don't mount and unmount components as often. <img width="856" alt="Screenshot 2024-02-22 at 17 10 07" src="1555d1e0
-93e6-4037-8636-470ea5101a14"> <img width="834" alt="Screenshot 2024-02-22 at 17 11 27" src="11db4f37
-e1c8-4ca5-83b7-c59d7937ce9a"> 2. Readability improvements Now it's much easier to see if the component is a drop zone or a draggable component. The logic that was mixed between these two is untangled and the components are smaller thanks to it. 4. Better API It's easier to add a `<Draggable/>` or `<Droppable/>` components. 6. Small bugs fixes Flash of colors when starting dragging in reorder group  Annotation cut content <img width="297" alt="Screenshot 2024-02-22 at 17 17 49" src="ce4ab3d1
-2a89-49cc-9230-c2ade194d1a3"> Half-transparent content when dragging - it doesn't make sense. <img width="284" alt="Screenshot 2024-02-22 at 17 16 51" src="fcafc12e
-2ec9-4f93-a224-c7b4c8852e09"> 7. Rewriting all the dnd tests from the package to rtl. It's still not ideal, but way more tested from user perspective and way more readable. --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
3baacf48d7
commit
095c0593c5
51 changed files with 2465 additions and 2800 deletions
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { DragDrop, DropOverlayWrapper, DropType, useDragDropContext } from '@kbn/dom-drag-drop';
|
||||
import { DropOverlayWrapper, DropType, Droppable, useDragDropContext } from '@kbn/dom-drag-drop';
|
||||
import { EuiEmptyPrompt, EuiPanel } from '@elastic/eui';
|
||||
|
||||
const DROP_PROPS = {
|
||||
|
@ -48,8 +48,7 @@ export const ExampleDropZone: React.FC<ExampleDropZoneProps> = ({ onDropField })
|
|||
const isDropAllowed = Boolean(onDroppingField);
|
||||
|
||||
return (
|
||||
<DragDrop
|
||||
draggable={false}
|
||||
<Droppable
|
||||
dropTypes={isDropAllowed ? DROP_PROPS.types : undefined}
|
||||
value={DROP_PROPS.value}
|
||||
order={DROP_PROPS.order}
|
||||
|
@ -68,6 +67,6 @@ export const ExampleDropZone: React.FC<ExampleDropZoneProps> = ({ onDropField })
|
|||
/>
|
||||
</EuiPanel>
|
||||
</DropOverlayWrapper>
|
||||
</DragDrop>
|
||||
</Droppable>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -32,27 +32,27 @@ This enables your child application to share the same drag / drop context as the
|
|||
|
||||
An item can be both draggable and droppable at the same time, but for simplicity's sake, we'll treat these two cases separately.
|
||||
|
||||
To enable dragging an item, use `DragDrop` with both a `draggable` and a `value` attribute. Property `value` has to be of a type object with a unique `id` property.
|
||||
To enable dragging an item, use `Draggable` with a `value` attribute. Property `value` has to be of a type object with a unique `id` property.
|
||||
|
||||
```js
|
||||
<div className="field-list">
|
||||
{fields.map((f) => (
|
||||
<DragDrop key={f.id} className="field-list-item" value={f} draggable>
|
||||
<Draggable key={f.id} className="field-list-item" value={f}>
|
||||
{f.name}
|
||||
</DragDrop>
|
||||
</Draggable>
|
||||
))}
|
||||
</div>
|
||||
```
|
||||
|
||||
## Dropping
|
||||
|
||||
To enable dropping, use `DragDrop` with both a `dropTypes` attribute that should be an array with at least one value and an `onDrop` handler attribute. `dropType` should only be truthy if is an item being dragged, and if a drop of the dragged item is supported.
|
||||
To enable dropping, use `Droppable` with both a `dropTypes` attribute that should be an array with at least one value and an `onDrop` handler attribute. `dropType` should only be truthy if is an item being dragged, and if a drop of the dragged item is supported.
|
||||
|
||||
```js
|
||||
const [ dndState ] = useDragDropContext()
|
||||
|
||||
return (
|
||||
<DragDrop
|
||||
<Droppable
|
||||
className="axis"
|
||||
dropTypes=['truthyValue']
|
||||
onDrop={(item) => onChange([...items, item])}
|
||||
|
@ -60,40 +60,48 @@ return (
|
|||
{items.map((x) => (
|
||||
<div>{x.name}</div>
|
||||
))}
|
||||
</DragDrop>
|
||||
</Droppable>
|
||||
);
|
||||
```
|
||||
|
||||
### Reordering
|
||||
|
||||
To create a reordering group, surround the elements from the same group with a `ReorderProvider`:
|
||||
To create a reordering group, the elements has to be surrounded with a `ReorderProvider`. They also need to be surrounded with draggable and droppable at the same time.
|
||||
|
||||
```js
|
||||
<ReorderProvider>... elements from one group here ...</ReorderProvider>
|
||||
```
|
||||
|
||||
The children `DragDrop` components must have props defined as in the example:
|
||||
The children `Draggable`/`Droppable` components must have props defined as in the example:
|
||||
|
||||
```js
|
||||
<ReorderProvider>
|
||||
<div className="field-list">
|
||||
{fields.map((f) => (
|
||||
<DragDrop
|
||||
<Draggable
|
||||
key={f.id}
|
||||
draggable
|
||||
dragType="move"
|
||||
dropTypes={["reorder"]} // generally shouldn't be set until a drag operation has started
|
||||
reorderableGroup={fields} // consists all reorderable elements in the group, eg. [{id:'3'}, {id:'5'}, {id:'1'}]
|
||||
value={{
|
||||
id: f.id,
|
||||
humanData: {
|
||||
label: 'Label'
|
||||
}
|
||||
}}
|
||||
onDrop={/*handler*/}
|
||||
>
|
||||
{f.name}
|
||||
</DragDrop>
|
||||
>
|
||||
<Droppable
|
||||
dropTypes={["reorder"]} // generally shouldn't be set until a drag operation has started
|
||||
reorderableGroup={fields} // consists all reorderable elements in the group, eg. [{id:'3'}, {id:'5'}, {id:'1'}]
|
||||
value={{
|
||||
id: f.id,
|
||||
humanData: {
|
||||
label: 'Label'
|
||||
}
|
||||
}}
|
||||
onDrop={/*handler*/}
|
||||
>
|
||||
{f.name}
|
||||
</Droppable>
|
||||
</Draggable>
|
||||
))}
|
||||
</div>
|
||||
</ReorderProvider>
|
||||
|
|
|
@ -14,7 +14,8 @@ export {
|
|||
type DraggingIdentifier,
|
||||
type DragDropAction,
|
||||
type DropOverlayWrapperProps,
|
||||
DragDrop,
|
||||
Draggable,
|
||||
Droppable,
|
||||
useDragDropContext,
|
||||
RootDragDropProvider,
|
||||
ChildDragDropProvider,
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DragDrop defined dropTypes is reflected in the className 1`] = `
|
||||
<div
|
||||
class="domDragDrop__container"
|
||||
data-test-subj="domDragDropContainer"
|
||||
>
|
||||
<button
|
||||
class="domDragDrop domDragDrop-isDroppable domDragDrop-isDropTarget"
|
||||
data-test-subj="domDragDrop"
|
||||
>
|
||||
Hello!
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DragDrop items that has dropTypes=undefined get special styling when another item is dragged 1`] = `
|
||||
<SingleDropInner
|
||||
className="domDragDrop domDragDrop-isDroppable domDragDrop-isNotDroppable"
|
||||
data-test-subj="testDragDrop"
|
||||
onDragEnter={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
onDragOver={[Function]}
|
||||
onDrop={[Function]}
|
||||
>
|
||||
<button
|
||||
className="domDragDrop domDragDrop-isDroppable domDragDrop-isNotDroppable"
|
||||
data-test-subj="testDragDrop"
|
||||
onDragEnter={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
onDragOver={[Function]}
|
||||
onDrop={[Function]}
|
||||
>
|
||||
Hello!
|
||||
</button>
|
||||
</SingleDropInner>
|
||||
`;
|
||||
|
||||
exports[`DragDrop renders if nothing is being dragged 1`] = `
|
||||
<div
|
||||
class=""
|
||||
data-test-subj="domDragDrop_draggable-hello"
|
||||
>
|
||||
<button
|
||||
aria-describedby="domDragDrop-keyboardInstructions"
|
||||
aria-label="hello"
|
||||
class="domDragDrop__keyboardHandler emotion-euiScreenReaderOnly"
|
||||
data-test-subj="domDragDrop-keyboardHandler"
|
||||
/>
|
||||
<button
|
||||
class="domDragDrop domDragDrop-isDraggable"
|
||||
data-test-subj="domDragDrop"
|
||||
draggable="true"
|
||||
>
|
||||
Hello!
|
||||
</button>
|
||||
</div>
|
||||
`;
|
File diff suppressed because it is too large
Load diff
|
@ -1,947 +0,0 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useContext, useCallback, useEffect, memo, useMemo, useState, useRef } from 'react';
|
||||
import type { KeyboardEvent, ReactElement } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { keys, EuiScreenReaderOnly, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
|
||||
import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect';
|
||||
import {
|
||||
DragDropIdentifier,
|
||||
DropIdentifier,
|
||||
nextValidDropTarget,
|
||||
ReorderContext,
|
||||
DropHandler,
|
||||
Ghost,
|
||||
RegisteredDropTargets,
|
||||
DragDropAction,
|
||||
DragContextState,
|
||||
useDragDropContext,
|
||||
} from './providers';
|
||||
import { DropType } from './types';
|
||||
import { REORDER_ITEM_MARGIN } from './constants';
|
||||
import './sass/drag_drop.scss';
|
||||
|
||||
/**
|
||||
* Droppable event
|
||||
*/
|
||||
export type DroppableEvent = React.DragEvent<HTMLElement>;
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
/**
|
||||
* The base props to the DragDrop component.
|
||||
*/
|
||||
interface BaseProps {
|
||||
/**
|
||||
* The CSS class(es) for the root element.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* CSS class to apply when the item is being dragged
|
||||
*/
|
||||
dragClassName?: string;
|
||||
|
||||
/**
|
||||
* The event handler that fires when an item
|
||||
* is dropped onto this DragDrop component.
|
||||
*/
|
||||
onDrop?: DropHandler;
|
||||
/**
|
||||
* The event handler that fires when this element is dragged.
|
||||
*/
|
||||
onDragStart?: (
|
||||
target?: DroppableEvent['currentTarget'] | KeyboardEvent<HTMLButtonElement>['currentTarget']
|
||||
) => void;
|
||||
/**
|
||||
* The event handler that fires when the dragging of this element ends.
|
||||
*/
|
||||
onDragEnd?: () => void;
|
||||
/**
|
||||
* The value associated with this item.
|
||||
*/
|
||||
value: DragDropIdentifier;
|
||||
|
||||
/**
|
||||
* The React element which will be passed the draggable handlers
|
||||
*/
|
||||
children: ReactElement;
|
||||
|
||||
/**
|
||||
* Disable any drag & drop behaviour
|
||||
*/
|
||||
isDisabled?: boolean;
|
||||
/**
|
||||
* 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 drop targets - when undefined, the currently dragged item
|
||||
* cannot be dropped onto this component.
|
||||
*/
|
||||
dropTypes?: DropType[];
|
||||
/**
|
||||
* Order for keyboard dragging. This takes an array of numbers which will be used to order hierarchically
|
||||
*/
|
||||
order: number[];
|
||||
/**
|
||||
* Extra drop targets by dropType
|
||||
*/
|
||||
getCustomDropTarget?: (dropType: DropType) => ReactElement | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The props for a draggable instance of that component.
|
||||
*/
|
||||
interface DragInnerProps extends BaseProps {
|
||||
dndDispatch: React.Dispatch<DragDropAction>;
|
||||
dataTestSubjPrefix?: string;
|
||||
activeDraggingProps?: {
|
||||
keyboardMode: boolean;
|
||||
activeDropTarget?: DropIdentifier;
|
||||
dropTargetsByOrder: RegisteredDropTargets;
|
||||
};
|
||||
extraKeyboardHandler?: (e: KeyboardEvent<HTMLButtonElement>) => void;
|
||||
ariaDescribedBy?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The props for a non-draggable instance of that component.
|
||||
*/
|
||||
interface DropsInnerProps extends BaseProps {
|
||||
dndState: DragContextState;
|
||||
dndDispatch: React.Dispatch<DragDropAction>;
|
||||
isNotDroppable: boolean;
|
||||
}
|
||||
|
||||
const REORDER_OFFSET = REORDER_ITEM_MARGIN / 2;
|
||||
|
||||
/**
|
||||
* DragDrop component
|
||||
* @param props
|
||||
* @constructor
|
||||
*/
|
||||
export const DragDrop = (props: BaseProps) => {
|
||||
const [dndState, dndDispatch] = useDragDropContext();
|
||||
|
||||
const { dragging, dropTargetsByOrder } = dndState;
|
||||
|
||||
if (props.isDisabled) {
|
||||
return props.children;
|
||||
}
|
||||
|
||||
const { value, draggable, dropTypes, reorderableGroup } = props;
|
||||
const isDragging = !!(draggable && value.id === dragging?.id);
|
||||
|
||||
const activeDraggingProps = isDragging
|
||||
? {
|
||||
keyboardMode: dndState.keyboardMode,
|
||||
activeDropTarget: dndState.activeDropTarget,
|
||||
dropTargetsByOrder,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
if (draggable && (!dropTypes || !dropTypes.length)) {
|
||||
const dragProps = {
|
||||
...props,
|
||||
activeDraggingProps,
|
||||
dataTestSubjPrefix: dndState.dataTestSubjPrefix,
|
||||
dndDispatch,
|
||||
};
|
||||
if (reorderableGroup && reorderableGroup.length > 1) {
|
||||
return <ReorderableDrag {...dragProps} reorderableGroup={reorderableGroup} />;
|
||||
} else {
|
||||
return <DragInner {...dragProps} />;
|
||||
}
|
||||
}
|
||||
|
||||
const dropProps = {
|
||||
...props,
|
||||
dndState,
|
||||
dndDispatch,
|
||||
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
|
||||
!!((!dropTypes || !dropTypes.length) && dragging && value.id !== dragging.id),
|
||||
};
|
||||
if (
|
||||
reorderableGroup &&
|
||||
reorderableGroup.length > 1 &&
|
||||
reorderableGroup?.some((i) => i.id === dragging?.id) &&
|
||||
dropTypes?.[0] === 'reorder'
|
||||
) {
|
||||
return <ReorderableDrop {...dropProps} reorderableGroup={reorderableGroup} />;
|
||||
}
|
||||
return <DropsInner {...dropProps} />;
|
||||
};
|
||||
|
||||
const removeSelection = () => {
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
}
|
||||
};
|
||||
|
||||
const DragInner = memo(function DragInner({
|
||||
dataTestSubj,
|
||||
className,
|
||||
dragClassName,
|
||||
value,
|
||||
children,
|
||||
dndDispatch,
|
||||
order,
|
||||
activeDraggingProps,
|
||||
dataTestSubjPrefix,
|
||||
dragType,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
extraKeyboardHandler,
|
||||
ariaDescribedBy,
|
||||
}: DragInnerProps) {
|
||||
const { keyboardMode, activeDropTarget, dropTargetsByOrder } = activeDraggingProps || {};
|
||||
|
||||
const setTarget = useCallback(
|
||||
(target?: DropIdentifier) => {
|
||||
if (!target) {
|
||||
dndDispatch({
|
||||
type: 'leaveDropTarget',
|
||||
});
|
||||
} else {
|
||||
dndDispatch({
|
||||
type: 'selectDropTarget',
|
||||
payload: {
|
||||
dropTarget: target,
|
||||
dragging: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
[dndDispatch, value]
|
||||
);
|
||||
|
||||
const setTargetOfIndex = useCallback(
|
||||
(id: string, index: number) => {
|
||||
const dropTargetsForActiveId =
|
||||
dropTargetsByOrder &&
|
||||
Object.values(dropTargetsByOrder).filter((dropTarget) => dropTarget?.id === id);
|
||||
setTarget(dropTargetsForActiveId?.[index]);
|
||||
},
|
||||
[dropTargetsByOrder, setTarget]
|
||||
);
|
||||
const modifierHandlers = useMemo(() => {
|
||||
const onKeyUp = (e: KeyboardEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
if (activeDropTarget?.id && ['Shift', 'Alt', 'Control'].includes(e.key)) {
|
||||
if (e.altKey) {
|
||||
setTargetOfIndex(activeDropTarget.id, 1);
|
||||
} else if (e.shiftKey) {
|
||||
setTargetOfIndex(activeDropTarget.id, 2);
|
||||
} else if (e.ctrlKey) {
|
||||
// the control option is available either for new or existing cases,
|
||||
// so need to offset based on some flags
|
||||
const offsetIndex =
|
||||
Number(activeDropTarget.humanData.canSwap) +
|
||||
Number(activeDropTarget.humanData.canDuplicate);
|
||||
setTargetOfIndex(activeDropTarget.id, offsetIndex + 1);
|
||||
} else {
|
||||
setTargetOfIndex(activeDropTarget.id, 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
const onKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
if (e.key === 'Alt' && activeDropTarget?.id) {
|
||||
setTargetOfIndex(activeDropTarget.id, 1);
|
||||
} else if (e.key === 'Shift' && activeDropTarget?.id) {
|
||||
setTargetOfIndex(activeDropTarget.id, 2);
|
||||
} else if (e.key === 'Control' && activeDropTarget?.id) {
|
||||
// the control option is available either for new or existing cases,
|
||||
// so need to offset based on some flags
|
||||
const offsetIndex =
|
||||
Number(activeDropTarget.humanData.canSwap) +
|
||||
Number(activeDropTarget.humanData.canDuplicate);
|
||||
setTargetOfIndex(activeDropTarget.id, offsetIndex + 1);
|
||||
}
|
||||
};
|
||||
return { onKeyDown, onKeyUp };
|
||||
}, [activeDropTarget, setTargetOfIndex]);
|
||||
|
||||
const dragStart = useCallback(
|
||||
(e: DroppableEvent | 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);
|
||||
|
||||
// Apply an optional class to the element being dragged so the ghost
|
||||
// can be styled. We must add it to the actual element for a single
|
||||
// frame before removing it so the ghost picks up the styling.
|
||||
const current = e.currentTarget;
|
||||
|
||||
if (dragClassName && !current.classList.contains(dragClassName)) {
|
||||
current.classList.add(dragClassName);
|
||||
requestAnimationFrame(() => {
|
||||
current.classList.remove(dragClassName);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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(() => {
|
||||
dndDispatch({
|
||||
type: 'startDragging',
|
||||
payload: {
|
||||
...(keyboardModeOn ? { keyboardMode: true } : {}),
|
||||
dragging: {
|
||||
...value,
|
||||
ghost: keyboardModeOn
|
||||
? {
|
||||
children,
|
||||
style: {
|
||||
width: currentTarget.offsetWidth,
|
||||
minHeight: currentTarget?.offsetHeight,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
onDragStart?.(currentTarget);
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[dndDispatch, value, onDragStart]
|
||||
);
|
||||
|
||||
const dragEnd = useCallback(
|
||||
(e?: DroppableEvent) => {
|
||||
e?.stopPropagation();
|
||||
|
||||
dndDispatch({
|
||||
type: 'endDragging',
|
||||
payload: { dragging: value },
|
||||
});
|
||||
onDragEnd?.();
|
||||
},
|
||||
[dndDispatch, value, onDragEnd]
|
||||
);
|
||||
|
||||
const setNextTarget = (e: KeyboardEvent<HTMLButtonElement>, reversed = false) => {
|
||||
const nextTarget = nextValidDropTarget(
|
||||
dropTargetsByOrder,
|
||||
activeDropTarget,
|
||||
[order.join(',')],
|
||||
(el) => el?.dropType !== 'reorder',
|
||||
reversed
|
||||
);
|
||||
|
||||
if (e.altKey && nextTarget?.id) {
|
||||
setTargetOfIndex(nextTarget.id, 1);
|
||||
} else if (e.shiftKey && nextTarget?.id) {
|
||||
setTargetOfIndex(nextTarget.id, 2);
|
||||
} else if (e.ctrlKey && nextTarget?.id) {
|
||||
setTargetOfIndex(nextTarget.id, 3);
|
||||
} else {
|
||||
setTarget(nextTarget);
|
||||
}
|
||||
};
|
||||
|
||||
const dropToActiveDropTarget = () => {
|
||||
if (activeDropTarget) {
|
||||
const { dropType, onDrop } = activeDropTarget;
|
||||
setTimeout(() => {
|
||||
dndDispatch({
|
||||
type: 'dropToTarget',
|
||||
payload: {
|
||||
dragging: value,
|
||||
dropTarget: activeDropTarget,
|
||||
},
|
||||
});
|
||||
});
|
||||
onDrop(value, dropType);
|
||||
}
|
||||
};
|
||||
|
||||
const shouldShowGhostImageInstead =
|
||||
dragType === 'move' &&
|
||||
keyboardMode &&
|
||||
activeDropTarget &&
|
||||
activeDropTarget.dropType !== 'reorder';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(className, {
|
||||
'domDragDrop-isHidden':
|
||||
(activeDraggingProps && dragType === 'move' && !keyboardMode) ||
|
||||
shouldShowGhostImageInstead,
|
||||
'domDragDrop--isDragStarted': activeDraggingProps,
|
||||
})}
|
||||
data-test-subj={`${dataTestSubjPrefix}_draggable-${value.humanData.label}`}
|
||||
>
|
||||
<EuiScreenReaderOnly showOnFocus>
|
||||
<button
|
||||
aria-label={value.humanData.label}
|
||||
aria-describedby={ariaDescribedBy || `${dataTestSubjPrefix}-keyboardInstructions`}
|
||||
className="domDragDrop__keyboardHandler"
|
||||
data-test-subj={`${dataTestSubjPrefix}-keyboardHandler`}
|
||||
onBlur={(e) => {
|
||||
if (activeDraggingProps) {
|
||||
dragEnd();
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e: 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) {
|
||||
if (keys.ARROW_LEFT === key || keys.ARROW_RIGHT === key) {
|
||||
setNextTarget(e, !!(keys.ARROW_LEFT === key));
|
||||
}
|
||||
modifierHandlers.onKeyDown(e);
|
||||
}
|
||||
}}
|
||||
onKeyUp={modifierHandlers.onKeyUp}
|
||||
/>
|
||||
</EuiScreenReaderOnly>
|
||||
|
||||
{React.cloneElement(children, {
|
||||
'data-test-subj': dataTestSubj || dataTestSubjPrefix,
|
||||
className: classNames(children.props.className, 'domDragDrop', 'domDragDrop-isDraggable'),
|
||||
draggable: true,
|
||||
onDragEnd: dragEnd,
|
||||
onDragStart: dragStart,
|
||||
onMouseDown: removeSelection,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const DropsInner = memo(function DropsInner(props: DropsInnerProps) {
|
||||
const {
|
||||
dataTestSubj,
|
||||
className,
|
||||
onDrop,
|
||||
value,
|
||||
children,
|
||||
draggable,
|
||||
dndState,
|
||||
dndDispatch,
|
||||
isNotDroppable,
|
||||
dropTypes,
|
||||
order,
|
||||
getAdditionalClassesOnEnter,
|
||||
getAdditionalClassesOnDroppable,
|
||||
getCustomDropTarget,
|
||||
} = props;
|
||||
|
||||
const { dragging, activeDropTarget, dataTestSubjPrefix, keyboardMode } = dndState;
|
||||
|
||||
const [isInZone, setIsInZone] = useState(false);
|
||||
const mainTargetRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useShallowCompareEffect(() => {
|
||||
if (dropTypes && dropTypes?.[0] && onDrop && keyboardMode) {
|
||||
dndDispatch({
|
||||
type: 'registerDropTargets',
|
||||
payload: dropTypes.reduce(
|
||||
(acc, dropType, index) => ({
|
||||
...acc,
|
||||
[[...props.order, index].join(',')]: { ...value, onDrop, dropType },
|
||||
}),
|
||||
{}
|
||||
),
|
||||
});
|
||||
}
|
||||
}, [order, dndDispatch, dropTypes, keyboardMode]);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
if (activeDropTarget && activeDropTarget.id !== value.id) {
|
||||
setIsInZone(false);
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (!activeDropTarget && isMounted) {
|
||||
setIsInZone(false);
|
||||
}
|
||||
}, 1000);
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [activeDropTarget, setIsInZone, value.id]);
|
||||
|
||||
const dragEnter = () => {
|
||||
if (!isInZone) {
|
||||
setIsInZone(true);
|
||||
}
|
||||
};
|
||||
|
||||
const getModifiedDropType = (e: DroppableEvent, dropType: DropType) => {
|
||||
if (!dropTypes || dropTypes.length <= 1) {
|
||||
return dropType;
|
||||
}
|
||||
const dropIndex = dropTypes.indexOf(dropType);
|
||||
if (dropIndex > 0) {
|
||||
return dropType;
|
||||
} else if (dropIndex === 0) {
|
||||
if (e.altKey && dropTypes[1]) {
|
||||
return dropTypes[1];
|
||||
} else if (e.shiftKey && dropTypes[2]) {
|
||||
return dropTypes[2];
|
||||
} else if (e.ctrlKey && (dropTypes.length > 3 ? dropTypes[3] : dropTypes[1])) {
|
||||
return dropTypes.length > 3 ? dropTypes[3] : dropTypes[1];
|
||||
}
|
||||
}
|
||||
return dropType;
|
||||
};
|
||||
|
||||
const dragOver = (e: DroppableEvent, dropType: DropType) => {
|
||||
e.preventDefault();
|
||||
if (!dragging || !onDrop) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modifiedDropType = getModifiedDropType(e, dropType);
|
||||
const isActiveDropTarget = !!(
|
||||
activeDropTarget?.id === value.id && activeDropTarget?.dropType === modifiedDropType
|
||||
);
|
||||
// An optimization to prevent a bunch of React churn.
|
||||
if (!isActiveDropTarget) {
|
||||
dndDispatch({
|
||||
type: 'selectDropTarget',
|
||||
payload: {
|
||||
dropTarget: { ...value, dropType: modifiedDropType, onDrop },
|
||||
dragging,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const dragLeave = () => {
|
||||
dndDispatch({ type: 'leaveDropTarget' });
|
||||
};
|
||||
|
||||
const drop = (e: DroppableEvent, dropType: DropType) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsInZone(false);
|
||||
if (onDrop && dragging) {
|
||||
const modifiedDropType = getModifiedDropType(e, dropType);
|
||||
onDrop(dragging, modifiedDropType);
|
||||
setTimeout(() => {
|
||||
dndDispatch({
|
||||
type: 'dropToTarget',
|
||||
payload: {
|
||||
dragging,
|
||||
dropTarget: { ...value, dropType: modifiedDropType, onDrop },
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
dndDispatch({ type: 'resetState' });
|
||||
};
|
||||
|
||||
const getProps = (dropType?: DropType, dropChildren?: ReactElement) => {
|
||||
const isActiveDropTarget = Boolean(
|
||||
activeDropTarget?.id === value.id && dropType === activeDropTarget?.dropType
|
||||
);
|
||||
return {
|
||||
'data-test-subj': dataTestSubj || dataTestSubjPrefix,
|
||||
className: getClasses(dropType, dropChildren),
|
||||
onDragEnter: dragEnter,
|
||||
onDragLeave: dragLeave,
|
||||
onDragOver: dropType ? (e: DroppableEvent) => dragOver(e, dropType) : noop,
|
||||
onDrop: dropType ? (e: DroppableEvent) => drop(e, dropType) : noop,
|
||||
draggable,
|
||||
ghost:
|
||||
(isActiveDropTarget && dropType !== 'reorder' && dragging?.ghost && dragging.ghost) ||
|
||||
undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const getClasses = (dropType?: DropType, dropChildren = children) => {
|
||||
const isActiveDropTarget = Boolean(
|
||||
activeDropTarget?.id === value.id && dropType === activeDropTarget?.dropType
|
||||
);
|
||||
const classesOnDroppable = getAdditionalClassesOnDroppable?.(dropType);
|
||||
|
||||
const classes = classNames(
|
||||
'domDragDrop',
|
||||
{
|
||||
'domDragDrop-isDraggable': draggable,
|
||||
'domDragDrop-isDroppable': !draggable,
|
||||
'domDragDrop-isDropTarget': dropType,
|
||||
'domDragDrop-isActiveDropTarget': dropType && isActiveDropTarget,
|
||||
'domDragDrop-isNotDroppable': isNotDroppable,
|
||||
},
|
||||
classesOnDroppable && { [classesOnDroppable]: dropType }
|
||||
);
|
||||
return classNames(classes, className, dropChildren.props.className);
|
||||
};
|
||||
|
||||
const getMainTargetClasses = () => {
|
||||
const classesOnEnter = getAdditionalClassesOnEnter?.(activeDropTarget?.dropType);
|
||||
return classNames(classesOnEnter && { [classesOnEnter]: activeDropTarget?.id === value.id });
|
||||
};
|
||||
|
||||
const mainTargetProps = getProps(dropTypes && dropTypes[0]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-test-subj={`${dataTestSubjPrefix}Container`}
|
||||
className={classNames('domDragDrop__container', {
|
||||
'domDragDrop__container-active': isInZone || activeDropTarget?.id === value.id,
|
||||
})}
|
||||
onDragEnter={dragEnter}
|
||||
ref={mainTargetRef}
|
||||
>
|
||||
<SingleDropInner
|
||||
{...mainTargetProps}
|
||||
className={classNames(mainTargetProps.className, getMainTargetClasses())}
|
||||
children={children}
|
||||
/>
|
||||
{dropTypes && dropTypes.length > 1 && (
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
direction="column"
|
||||
data-test-subj={`${dataTestSubjPrefix}ExtraDrops`}
|
||||
className={classNames('domDragDrop__extraDrops', {
|
||||
'domDragDrop__extraDrops-visible': isInZone || activeDropTarget?.id === value.id,
|
||||
})}
|
||||
>
|
||||
{dropTypes.slice(1).map((dropType) => {
|
||||
const dropChildren = getCustomDropTarget?.(dropType);
|
||||
return dropChildren ? (
|
||||
<EuiFlexItem key={dropType} className="domDragDrop__extraDropWrapper">
|
||||
<SingleDropInner {...getProps(dropType, dropChildren)}>
|
||||
{dropChildren}
|
||||
</SingleDropInner>
|
||||
</EuiFlexItem>
|
||||
) : null;
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const SingleDropInner = ({
|
||||
ghost,
|
||||
children,
|
||||
...rest
|
||||
}: {
|
||||
ghost?: Ghost;
|
||||
children: ReactElement;
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{React.cloneElement(children, rest)}
|
||||
{ghost
|
||||
? React.cloneElement(ghost.children, {
|
||||
className: classNames(ghost.children.props.className, 'domDragDrop_ghost'),
|
||||
style: ghost.style,
|
||||
})
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ReorderableDrag = memo(function ReorderableDrag(
|
||||
props: DragInnerProps & { reorderableGroup: Array<{ id: string }>; dragging?: DragDropIdentifier }
|
||||
) {
|
||||
const [{ isReorderOn, reorderedItems, direction }, reorderDispatch] = useContext(ReorderContext);
|
||||
|
||||
const { value, activeDraggingProps, reorderableGroup, dndDispatch, dataTestSubjPrefix } = props;
|
||||
|
||||
const { keyboardMode, activeDropTarget, dropTargetsByOrder } = activeDraggingProps || {};
|
||||
const isDragging = !!activeDraggingProps;
|
||||
|
||||
const isFocusInGroup = keyboardMode
|
||||
? isDragging &&
|
||||
(!activeDropTarget || reorderableGroup.some((i) => i.id === activeDropTarget?.id))
|
||||
: isDragging;
|
||||
|
||||
useEffect(() => {
|
||||
reorderDispatch({
|
||||
type: 'setIsReorderOn',
|
||||
payload: isFocusInGroup,
|
||||
});
|
||||
}, [reorderDispatch, isFocusInGroup]);
|
||||
|
||||
const onReorderableDragStart = (
|
||||
currentTarget?:
|
||||
| DroppableEvent['currentTarget']
|
||||
| KeyboardEvent<HTMLButtonElement>['currentTarget']
|
||||
) => {
|
||||
if (currentTarget) {
|
||||
setTimeout(() => {
|
||||
reorderDispatch({
|
||||
type: 'registerDraggingItemHeight',
|
||||
payload: currentTarget.offsetHeight + REORDER_OFFSET,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onReorderableDragEnd = () => {
|
||||
reorderDispatch({ type: 'reset' });
|
||||
};
|
||||
|
||||
const extraKeyboardHandler = (e: 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) {
|
||||
reorderDispatch({ type: 'reset' });
|
||||
} 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) {
|
||||
reorderDispatch({ type: 'reset' });
|
||||
dndDispatch({ type: 'leaveDropTarget' });
|
||||
return;
|
||||
}
|
||||
const droppingIndex = reorderableGroup.findIndex((i) => i.id === target.id);
|
||||
const draggingIndex = reorderableGroup.findIndex((i) => i.id === value?.id);
|
||||
if (draggingIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
dndDispatch({
|
||||
type: 'selectDropTarget',
|
||||
payload: {
|
||||
dropTarget: target,
|
||||
dragging: value,
|
||||
},
|
||||
});
|
||||
reorderDispatch({
|
||||
type: 'setReorderedItems',
|
||||
payload: { draggingIndex, droppingIndex, items: reorderableGroup },
|
||||
});
|
||||
};
|
||||
|
||||
const areItemsReordered = keyboardMode && isDragging && reorderedItems.length;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-test-subj={`${dataTestSubjPrefix}-reorderableDrag`}
|
||||
className={classNames('domDragDrop-reorderable', {
|
||||
['domDragDrop-translatableDrag']: isDragging,
|
||||
['domDragDrop-isKeyboardReorderInProgress']: keyboardMode && isDragging,
|
||||
})}
|
||||
style={
|
||||
areItemsReordered
|
||||
? {
|
||||
transform: `translateY(${direction === '+' ? '-' : '+'}${reorderedItems.reduce(
|
||||
(acc, el) => acc + (el.height ?? 0) + REORDER_OFFSET,
|
||||
0
|
||||
)}px)`,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<DragInner
|
||||
{...props}
|
||||
ariaDescribedBy={`${dataTestSubjPrefix}-keyboardInstructionsWithReorder`}
|
||||
extraKeyboardHandler={extraKeyboardHandler}
|
||||
onDragStart={onReorderableDragStart}
|
||||
onDragEnd={onReorderableDragEnd}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const ReorderableDrop = memo(function ReorderableDrop(
|
||||
props: DropsInnerProps & { reorderableGroup: Array<{ id: string }> }
|
||||
) {
|
||||
const { onDrop, value, dndState, dndDispatch, reorderableGroup } = props;
|
||||
|
||||
const { dragging, dataTestSubjPrefix, activeDropTarget } = dndState;
|
||||
const currentIndex = reorderableGroup.findIndex((i) => i.id === value.id);
|
||||
|
||||
const [{ isReorderOn, reorderedItems, draggingHeight, direction }, reorderDispatch] =
|
||||
useContext(ReorderContext);
|
||||
|
||||
const heightRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isReordered =
|
||||
isReorderOn && reorderedItems.some((el) => el.id === value.id) && reorderedItems.length;
|
||||
|
||||
useEffect(() => {
|
||||
if (isReordered && heightRef.current?.clientHeight) {
|
||||
reorderDispatch({
|
||||
type: 'registerReorderedItemHeight',
|
||||
payload: { id: value.id, height: heightRef.current.clientHeight },
|
||||
});
|
||||
}
|
||||
}, [isReordered, reorderDispatch, value.id]);
|
||||
|
||||
const onReorderableDragOver = (e: DroppableEvent) => {
|
||||
e.preventDefault();
|
||||
// An optimization to prevent a bunch of React churn.
|
||||
if (activeDropTarget?.id !== value?.id && onDrop) {
|
||||
const draggingIndex = reorderableGroup.findIndex((i) => i.id === dragging?.id);
|
||||
if (!dragging || draggingIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const droppingIndex = currentIndex;
|
||||
if (draggingIndex === droppingIndex) {
|
||||
reorderDispatch({ type: 'reset' });
|
||||
}
|
||||
|
||||
reorderDispatch({
|
||||
type: 'setReorderedItems',
|
||||
payload: { draggingIndex, droppingIndex, items: reorderableGroup },
|
||||
});
|
||||
dndDispatch({
|
||||
type: 'selectDropTarget',
|
||||
payload: {
|
||||
dropTarget: { ...value, dropType: 'reorder', onDrop },
|
||||
dragging,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onReorderableDrop = (e: DroppableEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (onDrop && dragging) {
|
||||
onDrop(dragging, 'reorder');
|
||||
// setTimeout ensures it will run after dragEnd messaging
|
||||
setTimeout(() => {
|
||||
dndDispatch({
|
||||
type: 'dropToTarget',
|
||||
payload: {
|
||||
dragging,
|
||||
dropTarget: { ...value, dropType: 'reorder', onDrop },
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
dndDispatch({ type: 'resetState' });
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={
|
||||
reorderedItems.some((i) => i.id === value.id)
|
||||
? {
|
||||
transform: `translateY(${direction}${draggingHeight}px)`,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
ref={heightRef}
|
||||
data-test-subj={`${dataTestSubjPrefix}-translatableDrop`}
|
||||
className="domDragDrop-translatableDrop domDragDrop-reorderable"
|
||||
>
|
||||
<DropsInner {...props} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-test-subj={`${dataTestSubjPrefix}-reorderableDropLayer`}
|
||||
className={classNames('domDragDrop', {
|
||||
['domDragDrop__reorderableDrop']: dragging,
|
||||
})}
|
||||
onDrop={onReorderableDrop}
|
||||
onDragOver={onReorderableDragOver}
|
||||
onDragLeave={() => {
|
||||
dndDispatch({ type: 'leaveDropTarget' });
|
||||
reorderDispatch({ type: 'reset' });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
360
packages/kbn-dom-drag-drop/src/drag_drop_reordering.test.tsx
Normal file
360
packages/kbn-dom-drag-drop/src/drag_drop_reordering.test.tsx
Normal file
|
@ -0,0 +1,360 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, screen, act } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { Droppable, DroppableProps } from './droppable';
|
||||
import { Draggable } from './draggable';
|
||||
import { dataTransfer, generateDragDropValue, renderWithDragDropContext } from './test_utils';
|
||||
import { ReorderProvider } from './providers/reorder_provider';
|
||||
|
||||
jest.useFakeTimers({ legacyFakeTimers: true });
|
||||
|
||||
const originalOffsetHeight = Object.getOwnPropertyDescriptor(
|
||||
HTMLElement.prototype,
|
||||
'clientHeight'
|
||||
) || { value: 0 };
|
||||
|
||||
const expectLabel = (label: string) =>
|
||||
expect.objectContaining({ humanData: expect.objectContaining({ label }) });
|
||||
|
||||
describe('Drag and drop reordering', () => {
|
||||
const onDrop = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
type MaximumThreeDroppablesProps = [
|
||||
Partial<DroppableProps>?,
|
||||
Partial<DroppableProps>?,
|
||||
Partial<DroppableProps>?
|
||||
];
|
||||
|
||||
const renderDragAndDropGroup = (
|
||||
propsOverrides: MaximumThreeDroppablesProps = [{}, {}, {}],
|
||||
contextOverrides = {}
|
||||
) => {
|
||||
const values = propsOverrides.map((props, index) => {
|
||||
return props?.value ? props.value : generateDragDropValue(`${index}`);
|
||||
});
|
||||
const reorderableGroup = values.map((value) => ({ id: value.id }));
|
||||
|
||||
const rtlRender = renderWithDragDropContext(
|
||||
<>
|
||||
<ReorderProvider dataTestSubj="domDragDrop">
|
||||
{propsOverrides.map((props, index) => {
|
||||
return (
|
||||
<Draggable
|
||||
key={index}
|
||||
value={values[index]}
|
||||
order={[index, 0]}
|
||||
dragType="move"
|
||||
reorderableGroup={reorderableGroup}
|
||||
>
|
||||
<Droppable
|
||||
order={[index]}
|
||||
onDrop={onDrop}
|
||||
dropTypes={['reorder']}
|
||||
{...props}
|
||||
value={values[index]}
|
||||
reorderableGroup={reorderableGroup}
|
||||
>
|
||||
<button>Element no{index}</button>
|
||||
</Droppable>
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
</ReorderProvider>
|
||||
|
||||
<Droppable
|
||||
order={[3, 0]}
|
||||
onDrop={onDrop}
|
||||
dropTypes={['duplicate_compatible']}
|
||||
value={generateDragDropValue()}
|
||||
reorderableGroup={reorderableGroup}
|
||||
>
|
||||
<button>Out of group</button>
|
||||
</Droppable>
|
||||
</>,
|
||||
undefined,
|
||||
contextOverrides
|
||||
);
|
||||
|
||||
const droppables = screen.queryAllByTestId('domDragDrop-reorderableDropLayer');
|
||||
const droppable = droppables[0];
|
||||
const draggableKeyboardHandlers = screen.queryAllByTestId('domDragDrop-keyboardHandler');
|
||||
const droppableContainers = screen.queryAllByTestId('domDragDropContainer');
|
||||
return {
|
||||
...rtlRender,
|
||||
droppableContainer: droppableContainers[0],
|
||||
startDragging: (index = 0) => {
|
||||
const draggable = screen.getByTestId(`domDragDrop_domDraggable_${index}`);
|
||||
fireEvent.dragStart(draggable, { dataTransfer });
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
drop: (droppableIndex = 0, options = {}) => {
|
||||
const dropEvent = new MouseEvent('drop', { ...options, bubbles: true });
|
||||
fireEvent(droppables[droppableIndex], dropEvent);
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
dragOver: (droppableIndex = 0, options = {}) => {
|
||||
fireEvent.dragOver(droppables[droppableIndex], options);
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
dragLeave: (droppableIndex = 0) => {
|
||||
fireEvent.dragLeave(droppables[droppableIndex]);
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
startDraggingByKeyboard: (index = 0) => {
|
||||
draggableKeyboardHandlers[index].focus();
|
||||
userEvent.keyboard('{enter}');
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
dropByKeyboard: () => {
|
||||
userEvent.keyboard('{enter}');
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
cancelByKeyboard: () => {
|
||||
userEvent.keyboard('{esc}');
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
reorderDownByKeyboard: () => {
|
||||
userEvent.keyboard('{arrowdown}');
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
reorderUpByKeyboard: () => {
|
||||
userEvent.keyboard('{arrowup}');
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
dragOverToNextByKeyboard: () => {
|
||||
userEvent.keyboard('{arrowright}');
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
dragOverToPreviousByKeyboard: () => {
|
||||
userEvent.keyboard('{arrowleft}');
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
pressModifierKey: (key: '{Shift}' | '{Alt}' | '{Ctrl}') => {
|
||||
userEvent.keyboard(key);
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
droppable,
|
||||
droppables,
|
||||
};
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(HTMLElement.prototype, 'clientHeight', {
|
||||
configurable: true,
|
||||
value: 40,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
Object.defineProperty(HTMLElement.prototype, 'clientHeight', originalOffsetHeight);
|
||||
});
|
||||
|
||||
test('runs onDrop when the element is dropped', () => {
|
||||
const { startDragging, drop } = renderDragAndDropGroup();
|
||||
startDragging(0);
|
||||
drop(1);
|
||||
expect(onDrop).toBeCalled();
|
||||
});
|
||||
|
||||
test('reordered elements get extra styling showing the new position', () => {
|
||||
const { startDragging, dragOver } = renderDragAndDropGroup();
|
||||
startDragging(0);
|
||||
dragOver(1);
|
||||
|
||||
expect(screen.getAllByTestId('domDragDrop-translatableDrop')[1]).toHaveStyle({
|
||||
transform: 'translateY(-48px)',
|
||||
});
|
||||
expect(screen.getAllByTestId('domDragDrop-translatableDrop')[2]).not.toHaveStyle({
|
||||
transform: 'translateY(-48px)',
|
||||
});
|
||||
dragOver(2);
|
||||
expect(screen.getAllByTestId('domDragDrop-translatableDrop')[1]).toHaveStyle({
|
||||
transform: 'translateY(-48px)',
|
||||
});
|
||||
expect(screen.getAllByTestId('domDragDrop-translatableDrop')[2]).toHaveStyle({
|
||||
transform: 'translateY(-48px)',
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyboard mode', () => {
|
||||
test('doesn`t run onDrop when dropping into an original position without any other movements', () => {
|
||||
const { startDraggingByKeyboard, dropByKeyboard } = renderDragAndDropGroup();
|
||||
// 0 -> 0
|
||||
startDraggingByKeyboard(0);
|
||||
dropByKeyboard();
|
||||
expect(onDrop).not.toBeCalled();
|
||||
});
|
||||
test('doesn`t run onDrop when dropping into an original position after some movements', () => {
|
||||
const {
|
||||
startDraggingByKeyboard,
|
||||
dropByKeyboard,
|
||||
reorderDownByKeyboard,
|
||||
reorderUpByKeyboard,
|
||||
} = renderDragAndDropGroup();
|
||||
// 1 -> 1
|
||||
startDraggingByKeyboard(1);
|
||||
reorderDownByKeyboard();
|
||||
reorderUpByKeyboard();
|
||||
dropByKeyboard();
|
||||
expect(onDrop).not.toBeCalled();
|
||||
});
|
||||
test('doesn’t run onDrop when the movement is cancelled', () => {
|
||||
const { startDraggingByKeyboard, reorderDownByKeyboard, cancelByKeyboard } =
|
||||
renderDragAndDropGroup();
|
||||
// 1 -> x
|
||||
startDraggingByKeyboard(0);
|
||||
reorderDownByKeyboard();
|
||||
reorderDownByKeyboard();
|
||||
cancelByKeyboard();
|
||||
expect(onDrop).not.toBeCalled();
|
||||
});
|
||||
test('runs onDrop when the element is reordered and dropped', () => {
|
||||
const {
|
||||
startDraggingByKeyboard,
|
||||
dropByKeyboard,
|
||||
reorderDownByKeyboard,
|
||||
reorderUpByKeyboard,
|
||||
} = renderDragAndDropGroup();
|
||||
// 0--> 2
|
||||
startDraggingByKeyboard(0);
|
||||
reorderDownByKeyboard();
|
||||
reorderDownByKeyboard();
|
||||
dropByKeyboard();
|
||||
expect(onDrop).toBeCalledWith(expectLabel('0'), 'reorder');
|
||||
|
||||
// 2 --> 0
|
||||
startDraggingByKeyboard(2);
|
||||
reorderUpByKeyboard();
|
||||
reorderUpByKeyboard();
|
||||
dropByKeyboard();
|
||||
expect(onDrop).toBeCalledWith(expectLabel('2'), 'reorder');
|
||||
});
|
||||
test('reordered elements get extra styling showing the new position from element 0 to element 2', () => {
|
||||
const { startDraggingByKeyboard, reorderDownByKeyboard } = renderDragAndDropGroup();
|
||||
// 0--> 2
|
||||
startDraggingByKeyboard(0);
|
||||
reorderDownByKeyboard();
|
||||
expect(screen.getAllByTestId('domDragDrop-reorderableDrag')[0]).toHaveStyle({
|
||||
transform: 'translateY(+48px)',
|
||||
});
|
||||
expect(screen.getAllByTestId('domDragDrop-translatableDrop')[1]).toHaveStyle({
|
||||
transform: 'translateY(-48px)',
|
||||
});
|
||||
expect(screen.getByText('Element no1')).toHaveClass('domDroppable--hover');
|
||||
reorderDownByKeyboard();
|
||||
expect(screen.getAllByTestId('domDragDrop-reorderableDrag')[0]).toHaveStyle({
|
||||
transform: 'translateY(+96px)',
|
||||
});
|
||||
expect(screen.getAllByTestId('domDragDrop-translatableDrop')[1]).toHaveStyle({
|
||||
transform: 'translateY(-48px)',
|
||||
});
|
||||
expect(screen.getAllByTestId('domDragDrop-translatableDrop')[2]).toHaveStyle({
|
||||
transform: 'translateY(-48px)',
|
||||
});
|
||||
expect(screen.getByText('Element no2')).toHaveClass('domDroppable--hover');
|
||||
});
|
||||
|
||||
test('reordered elements get extra styling showing the new position from element 2 to element 0', () => {
|
||||
const { startDraggingByKeyboard, reorderUpByKeyboard } = renderDragAndDropGroup();
|
||||
// 2 --> 0
|
||||
startDraggingByKeyboard(2);
|
||||
reorderUpByKeyboard();
|
||||
expect(screen.getAllByTestId('domDragDrop-reorderableDrag')[2]).toHaveStyle({
|
||||
transform: 'translateY(-48px)',
|
||||
});
|
||||
expect(screen.getAllByTestId('domDragDrop-translatableDrop')[1]).toHaveStyle({
|
||||
transform: 'translateY(+48px)',
|
||||
});
|
||||
|
||||
expect(screen.getByText('Element no1')).toHaveClass('domDroppable--hover');
|
||||
reorderUpByKeyboard();
|
||||
expect(screen.getAllByTestId('domDragDrop-reorderableDrag')[2]).toHaveStyle({
|
||||
transform: 'translateY(-96px)',
|
||||
});
|
||||
expect(screen.getAllByTestId('domDragDrop-translatableDrop')[1]).toHaveStyle({
|
||||
transform: 'translateY(+48px)',
|
||||
});
|
||||
expect(screen.getAllByTestId('domDragDrop-translatableDrop')[0]).toHaveStyle({
|
||||
transform: 'translateY(+48px)',
|
||||
});
|
||||
expect(screen.getByText('Element no0')).toHaveClass('domDroppable--hover');
|
||||
});
|
||||
|
||||
test('reorders through all the drop targets and then stops at the last element', () => {
|
||||
const {
|
||||
startDraggingByKeyboard,
|
||||
reorderDownByKeyboard,
|
||||
cancelByKeyboard,
|
||||
reorderUpByKeyboard,
|
||||
} = renderDragAndDropGroup();
|
||||
startDraggingByKeyboard();
|
||||
reorderDownByKeyboard();
|
||||
reorderDownByKeyboard();
|
||||
reorderDownByKeyboard();
|
||||
reorderDownByKeyboard();
|
||||
|
||||
expect(screen.getByText('Element no2')).toHaveClass('domDroppable--hover');
|
||||
cancelByKeyboard();
|
||||
startDraggingByKeyboard(2);
|
||||
reorderUpByKeyboard();
|
||||
reorderUpByKeyboard();
|
||||
reorderUpByKeyboard();
|
||||
reorderUpByKeyboard();
|
||||
expect(screen.getByText('Element no0')).toHaveClass('domDroppable--hover');
|
||||
});
|
||||
|
||||
test('exits reordering and selects out of group target when hitting arrow left', () => {
|
||||
const {
|
||||
startDraggingByKeyboard,
|
||||
cancelByKeyboard,
|
||||
dragOverToPreviousByKeyboard,
|
||||
dragOverToNextByKeyboard,
|
||||
} = renderDragAndDropGroup();
|
||||
|
||||
startDraggingByKeyboard();
|
||||
dragOverToNextByKeyboard();
|
||||
expect(screen.getByText('Out of group')).toHaveClass('domDroppable--hover');
|
||||
cancelByKeyboard();
|
||||
startDraggingByKeyboard();
|
||||
dragOverToPreviousByKeyboard();
|
||||
expect(screen.getByText('Out of group')).toHaveClass('domDroppable--hover');
|
||||
});
|
||||
});
|
||||
});
|
137
packages/kbn-dom-drag-drop/src/draggable.test.tsx
Normal file
137
packages/kbn-dom-drag-drop/src/draggable.test.tsx
Normal file
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Draggable } from './draggable';
|
||||
import { Droppable } from './droppable';
|
||||
import {
|
||||
generateDragDropValue,
|
||||
renderWithDragDropContext,
|
||||
dataTransfer,
|
||||
EXACT,
|
||||
} from './test_utils';
|
||||
|
||||
jest.useFakeTimers({ legacyFakeTimers: true });
|
||||
|
||||
describe('Draggable', () => {
|
||||
const renderDraggable = (propsOverrides = {}) => {
|
||||
const rtlRender = renderWithDragDropContext(
|
||||
<>
|
||||
<Draggable
|
||||
dragType="move"
|
||||
value={generateDragDropValue('drag_this')}
|
||||
order={[2, 0, 1, 0]}
|
||||
{...propsOverrides}
|
||||
>
|
||||
<button>Drag this</button>
|
||||
</Draggable>
|
||||
<Droppable
|
||||
order={[2, 0, 1, 1]}
|
||||
value={generateDragDropValue()}
|
||||
onDrop={jest.fn()}
|
||||
dropTypes={['field_replace']}
|
||||
>
|
||||
<button>Drop here</button>
|
||||
</Droppable>
|
||||
</>
|
||||
);
|
||||
|
||||
const draggable = screen.getByTestId('domDragDrop_domDraggable_drag_this');
|
||||
const draggableKeyboardHandler = screen.getByTestId('domDragDrop-keyboardHandler');
|
||||
const droppable = screen.getByTestId('domDragDrop-domDroppable');
|
||||
|
||||
return {
|
||||
...rtlRender,
|
||||
draggable,
|
||||
droppable,
|
||||
startDragging: () => {
|
||||
fireEvent.dragStart(draggable, { dataTransfer });
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
startDraggingByKeyboard: () => {
|
||||
draggableKeyboardHandler.focus();
|
||||
userEvent.keyboard('{enter}');
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
dragOverToNextByKeyboard: () => {
|
||||
userEvent.keyboard('{arrowright}');
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
endDragging: () => {
|
||||
fireEvent.dragEnd(draggable, { dataTransfer });
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
dragOver: () => {
|
||||
fireEvent.dragOver(droppable);
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('makes component draggable', () => {
|
||||
const { draggable } = renderDraggable();
|
||||
expect(draggable).toHaveProperty('draggable', true);
|
||||
});
|
||||
|
||||
test('removes selection on mouse down before dragging', async () => {
|
||||
const removeAllRanges = jest.fn();
|
||||
global.getSelection = jest.fn(() => ({ removeAllRanges } as unknown as Selection));
|
||||
const { draggable } = renderDraggable();
|
||||
fireEvent.mouseDown(draggable);
|
||||
expect(global.getSelection).toBeCalled();
|
||||
expect(removeAllRanges).toBeCalled();
|
||||
});
|
||||
|
||||
test('on drag start, sets text in dataTransfer', async () => {
|
||||
const { startDragging } = renderDraggable();
|
||||
|
||||
startDragging();
|
||||
expect(dataTransfer.setData).toBeCalledWith('text', 'drag_this');
|
||||
});
|
||||
test('className is added when draggable is being dragged', async () => {
|
||||
const { startDragging, draggable, endDragging } = renderDraggable({
|
||||
dragClassName: 'dragTest',
|
||||
});
|
||||
expect(draggable).toHaveClass('domDraggable', EXACT);
|
||||
startDragging();
|
||||
expect(draggable).toHaveClass('domDraggable domDraggable_active--move', EXACT);
|
||||
endDragging();
|
||||
expect(draggable).toHaveClass('domDraggable', EXACT);
|
||||
});
|
||||
|
||||
describe('keyboard mode', () => {
|
||||
test('dragClassName is added to ghost when element is dragged', async () => {
|
||||
const { startDraggingByKeyboard, dragOverToNextByKeyboard, droppable } = renderDraggable({
|
||||
dragClassName: 'dragTest',
|
||||
});
|
||||
startDraggingByKeyboard();
|
||||
dragOverToNextByKeyboard();
|
||||
expect(droppable).toHaveClass('domDroppable domDroppable--active domDroppable--hover', EXACT);
|
||||
expect(within(screen.getByTestId('domDragDropContainer')).getByText('Drag this')).toHaveClass(
|
||||
'dragTest'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
545
packages/kbn-dom-drag-drop/src/draggable.tsx
Normal file
545
packages/kbn-dom-drag-drop/src/draggable.tsx
Normal file
|
@ -0,0 +1,545 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useContext, useCallback, useEffect, memo, useMemo } from 'react';
|
||||
import type { KeyboardEvent, ReactElement } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { keys, EuiScreenReaderOnly } from '@elastic/eui';
|
||||
import {
|
||||
DragDropIdentifier,
|
||||
DropIdentifier,
|
||||
nextValidDropTarget,
|
||||
ReorderContext,
|
||||
RegisteredDropTargets,
|
||||
DragDropAction,
|
||||
useDragDropContext,
|
||||
} from './providers';
|
||||
import { REORDER_ITEM_MARGIN } from './constants';
|
||||
import './sass/draggable.scss';
|
||||
|
||||
type DragEvent = React.DragEvent<HTMLElement>;
|
||||
|
||||
/**
|
||||
* The base props to the Draggable component.
|
||||
*/
|
||||
interface DraggableProps {
|
||||
/**
|
||||
* The CSS class(es) for the root element.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* CSS class to apply when the item is being dragged
|
||||
*/
|
||||
dragClassName?: string;
|
||||
|
||||
/**
|
||||
* The event handler that fires when this element is dragged.
|
||||
*/
|
||||
onDragStart?: (
|
||||
target?: DragEvent['currentTarget'] | KeyboardEvent<HTMLButtonElement>['currentTarget']
|
||||
) => void;
|
||||
/**
|
||||
* The event handler that fires when the dragging of this element ends.
|
||||
*/
|
||||
onDragEnd?: () => void;
|
||||
/**
|
||||
* The value associated with this item.
|
||||
*/
|
||||
value: DragDropIdentifier;
|
||||
|
||||
/**
|
||||
* The React element which will be passed the draggable handlers
|
||||
*/
|
||||
children: ReactElement;
|
||||
|
||||
/**
|
||||
* Disable any drag & drop behaviour
|
||||
*/
|
||||
isDisabled?: boolean;
|
||||
|
||||
/**
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* 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 DraggableImplProps extends DraggableProps {
|
||||
dndDispatch: React.Dispatch<DragDropAction>;
|
||||
dataTestSubjPrefix?: string;
|
||||
draggedItemProps?: {
|
||||
keyboardMode: boolean;
|
||||
hoveredDropTarget?: DropIdentifier;
|
||||
dropTargetsByOrder: RegisteredDropTargets;
|
||||
};
|
||||
extraKeyboardHandler?: (e: KeyboardEvent<HTMLButtonElement>) => void;
|
||||
ariaDescribedBy?: string;
|
||||
}
|
||||
|
||||
const REORDER_OFFSET = REORDER_ITEM_MARGIN / 2;
|
||||
|
||||
/**
|
||||
* Draggable component
|
||||
* @param props
|
||||
* @constructor
|
||||
*/
|
||||
export const Draggable = ({ reorderableGroup, ...props }: DraggableProps) => {
|
||||
const [
|
||||
{ dragging, dropTargetsByOrder, hoveredDropTarget, keyboardMode, dataTestSubjPrefix },
|
||||
dndDispatch,
|
||||
] = useDragDropContext();
|
||||
|
||||
if (props.isDisabled) {
|
||||
return props.children;
|
||||
}
|
||||
|
||||
const isDragging = props.value.id === dragging?.id;
|
||||
|
||||
const draggableProps = {
|
||||
...props,
|
||||
draggedItemProps: isDragging
|
||||
? {
|
||||
keyboardMode,
|
||||
hoveredDropTarget,
|
||||
dropTargetsByOrder,
|
||||
}
|
||||
: undefined,
|
||||
dataTestSubjPrefix,
|
||||
dndDispatch,
|
||||
};
|
||||
if (reorderableGroup && reorderableGroup.length > 1) {
|
||||
return <ReorderableDraggableImpl {...draggableProps} reorderableGroup={reorderableGroup} />;
|
||||
} else {
|
||||
return <DraggableImpl {...draggableProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
const removeSelection = () => {
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
}
|
||||
};
|
||||
|
||||
const DraggableImpl = memo(function DraggableImpl({
|
||||
dataTestSubj,
|
||||
className,
|
||||
dragClassName,
|
||||
value,
|
||||
children,
|
||||
dndDispatch,
|
||||
order,
|
||||
draggedItemProps,
|
||||
dataTestSubjPrefix,
|
||||
dragType,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
extraKeyboardHandler,
|
||||
ariaDescribedBy,
|
||||
}: DraggableImplProps) {
|
||||
const { keyboardMode, hoveredDropTarget, dropTargetsByOrder } = draggedItemProps || {};
|
||||
|
||||
const setTarget = useCallback(
|
||||
(target?: DropIdentifier) => {
|
||||
if (!target) {
|
||||
dndDispatch({
|
||||
type: 'leaveDropTarget',
|
||||
});
|
||||
} else {
|
||||
dndDispatch({
|
||||
type: 'selectDropTarget',
|
||||
payload: {
|
||||
dropTarget: target,
|
||||
dragging: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
[dndDispatch, value]
|
||||
);
|
||||
|
||||
const setTargetOfIndex = useCallback(
|
||||
(id: string, index: number) => {
|
||||
const dropTargetsForActiveId =
|
||||
dropTargetsByOrder &&
|
||||
Object.values(dropTargetsByOrder).filter((dropTarget) => dropTarget?.id === id);
|
||||
setTarget(dropTargetsForActiveId?.[index]);
|
||||
},
|
||||
[dropTargetsByOrder, setTarget]
|
||||
);
|
||||
const modifierHandlers = useMemo(() => {
|
||||
const onKeyUp = (e: KeyboardEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
if (hoveredDropTarget?.id && ['Shift', 'Alt', 'Control'].includes(e.key)) {
|
||||
if (e.altKey) {
|
||||
setTargetOfIndex(hoveredDropTarget.id, 1);
|
||||
} else if (e.shiftKey) {
|
||||
setTargetOfIndex(hoveredDropTarget.id, 2);
|
||||
} else if (e.ctrlKey) {
|
||||
// the control option is available either for new or existing cases,
|
||||
// so need to offset based on some flags
|
||||
const offsetIndex =
|
||||
Number(hoveredDropTarget.humanData.canSwap) +
|
||||
Number(hoveredDropTarget.humanData.canDuplicate);
|
||||
setTargetOfIndex(hoveredDropTarget.id, offsetIndex + 1);
|
||||
} else {
|
||||
setTargetOfIndex(hoveredDropTarget.id, 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
const onKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
if (e.key === 'Alt' && hoveredDropTarget?.id) {
|
||||
setTargetOfIndex(hoveredDropTarget.id, 1);
|
||||
} else if (e.key === 'Shift' && hoveredDropTarget?.id) {
|
||||
setTargetOfIndex(hoveredDropTarget.id, 2);
|
||||
} else if (e.key === 'Control' && hoveredDropTarget?.id) {
|
||||
// the control option is available either for new or existing cases,
|
||||
// so need to offset based on some flags
|
||||
const offsetIndex =
|
||||
Number(hoveredDropTarget.humanData.canSwap) +
|
||||
Number(hoveredDropTarget.humanData.canDuplicate);
|
||||
setTargetOfIndex(hoveredDropTarget.id, offsetIndex + 1);
|
||||
}
|
||||
};
|
||||
return { onKeyDown, onKeyUp };
|
||||
}, [hoveredDropTarget, setTargetOfIndex]);
|
||||
|
||||
const dragStart = useCallback(
|
||||
(e: DragEvent | 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;
|
||||
onDragStart?.(e?.currentTarget);
|
||||
|
||||
// Apply an optional class to the element being dragged so the ghost
|
||||
// can be styled. We must add it to the actual element for a single
|
||||
// frame before removing it so the ghost picks up the styling.
|
||||
const current = e.currentTarget;
|
||||
|
||||
if (dragClassName && !current.classList.contains(dragClassName)) {
|
||||
current.classList.add(dragClassName);
|
||||
requestAnimationFrame(() => {
|
||||
current.classList.remove(dragClassName);
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
dndDispatch({
|
||||
type: 'startDragging',
|
||||
payload: {
|
||||
...(keyboardModeOn ? { keyboardMode: true } : {}),
|
||||
dragging: {
|
||||
...value,
|
||||
ghost: keyboardModeOn
|
||||
? {
|
||||
children,
|
||||
className: classNames(dragClassName),
|
||||
style: {
|
||||
width: currentTarget.offsetWidth,
|
||||
minHeight: currentTarget?.offsetHeight,
|
||||
zIndex: 1000,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[dndDispatch, value, onDragStart]
|
||||
);
|
||||
|
||||
const dragEnd = useCallback(
|
||||
(e?: DragEvent) => {
|
||||
e?.stopPropagation();
|
||||
|
||||
dndDispatch({
|
||||
type: 'endDragging',
|
||||
payload: { dragging: value },
|
||||
});
|
||||
onDragEnd?.();
|
||||
},
|
||||
[dndDispatch, value, onDragEnd]
|
||||
);
|
||||
|
||||
const setNextTarget = (e: KeyboardEvent<HTMLButtonElement>, reversed = false) => {
|
||||
const nextTarget = nextValidDropTarget(
|
||||
dropTargetsByOrder,
|
||||
hoveredDropTarget,
|
||||
[order.join(',')],
|
||||
(el) => el?.dropType !== 'reorder',
|
||||
reversed
|
||||
);
|
||||
if (typeof nextTarget === 'string' || nextTarget === undefined) {
|
||||
return setTarget(undefined);
|
||||
} else if (e.altKey) {
|
||||
return setTargetOfIndex(nextTarget.id, 1);
|
||||
} else if (e.shiftKey) {
|
||||
return setTargetOfIndex(nextTarget.id, 2);
|
||||
} else if (e.ctrlKey) {
|
||||
return setTargetOfIndex(nextTarget.id, 3);
|
||||
}
|
||||
return setTarget(nextTarget);
|
||||
};
|
||||
|
||||
const dropToSelectedDropTarget = () => {
|
||||
if (hoveredDropTarget) {
|
||||
const { dropType, onDrop } = hoveredDropTarget;
|
||||
setTimeout(() => {
|
||||
dndDispatch({
|
||||
type: 'dropToTarget',
|
||||
payload: {
|
||||
dragging: value,
|
||||
dropTarget: hoveredDropTarget,
|
||||
},
|
||||
});
|
||||
});
|
||||
onDrop(value, dropType);
|
||||
}
|
||||
};
|
||||
|
||||
const shouldShowGhostImageInstead =
|
||||
dragType === 'move' &&
|
||||
keyboardMode &&
|
||||
hoveredDropTarget &&
|
||||
hoveredDropTarget.dropType !== 'reorder';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(className, 'domDraggable', {
|
||||
'domDraggable_active--move': draggedItemProps && dragType === 'move' && !keyboardMode,
|
||||
'domDraggable_dragover_keyboard--move': shouldShowGhostImageInstead,
|
||||
'domDraggable_active--copy': draggedItemProps && dragType === 'copy' && !keyboardMode,
|
||||
'domDraggable_dragover_keyboard--copy':
|
||||
keyboardMode && draggedItemProps && hoveredDropTarget,
|
||||
})}
|
||||
data-test-subj={dataTestSubj || `${dataTestSubjPrefix}_domDraggable_${value.humanData.label}`}
|
||||
draggable
|
||||
onDragEnd={dragEnd}
|
||||
onDragStart={dragStart}
|
||||
onMouseDown={removeSelection}
|
||||
>
|
||||
<EuiScreenReaderOnly showOnFocus>
|
||||
<button
|
||||
aria-label={value.humanData.label}
|
||||
aria-describedby={ariaDescribedBy || `${dataTestSubjPrefix}-keyboardInstructions`}
|
||||
className="domDraggable__keyboardHandler"
|
||||
data-test-subj={`${dataTestSubjPrefix}-keyboardHandler`}
|
||||
onBlur={(e) => {
|
||||
if (draggedItemProps) {
|
||||
dragEnd();
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e: KeyboardEvent<HTMLButtonElement>) => {
|
||||
const { key } = e;
|
||||
if (key === keys.ENTER || key === keys.SPACE) {
|
||||
if (hoveredDropTarget) {
|
||||
dropToSelectedDropTarget();
|
||||
}
|
||||
|
||||
if (draggedItemProps) {
|
||||
dragEnd();
|
||||
} else {
|
||||
dragStart(e, true);
|
||||
}
|
||||
} else if (key === keys.ESCAPE) {
|
||||
if (draggedItemProps) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
dragEnd();
|
||||
}
|
||||
}
|
||||
if (extraKeyboardHandler) {
|
||||
extraKeyboardHandler(e);
|
||||
}
|
||||
if (keyboardMode) {
|
||||
if (keys.ARROW_LEFT === key || keys.ARROW_RIGHT === key) {
|
||||
setNextTarget(e, !!(keys.ARROW_LEFT === key));
|
||||
}
|
||||
modifierHandlers.onKeyDown(e);
|
||||
}
|
||||
}}
|
||||
onKeyUp={modifierHandlers.onKeyUp}
|
||||
/>
|
||||
</EuiScreenReaderOnly>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const ReorderableDraggableImpl = memo(function ReorderableDraggableImpl(
|
||||
props: DraggableImplProps & {
|
||||
reorderableGroup: Array<{ id: string }>;
|
||||
dragging?: DragDropIdentifier;
|
||||
}
|
||||
) {
|
||||
const [{ isReorderOn, reorderedItems, direction }, reorderDispatch] = useContext(ReorderContext);
|
||||
|
||||
const { value, draggedItemProps, reorderableGroup, dndDispatch, dataTestSubjPrefix } = props;
|
||||
|
||||
const { keyboardMode, hoveredDropTarget, dropTargetsByOrder } = draggedItemProps || {};
|
||||
const isDragging = !!draggedItemProps;
|
||||
|
||||
const isFocusInGroup = keyboardMode
|
||||
? isDragging &&
|
||||
(!hoveredDropTarget || reorderableGroup.some((i) => i.id === hoveredDropTarget?.id))
|
||||
: isDragging;
|
||||
|
||||
useEffect(() => {
|
||||
return () => reorderDispatch({ type: 'dragEnd' });
|
||||
}, [reorderDispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
reorderDispatch({
|
||||
type: 'setIsReorderOn',
|
||||
payload: isFocusInGroup,
|
||||
});
|
||||
}, [reorderDispatch, isFocusInGroup]);
|
||||
|
||||
const onReorderableDragStart = (
|
||||
currentTarget?: DragEvent['currentTarget'] | KeyboardEvent<HTMLButtonElement>['currentTarget']
|
||||
) => {
|
||||
if (currentTarget) {
|
||||
setTimeout(() => {
|
||||
reorderDispatch({
|
||||
type: 'registerDraggingItemHeight',
|
||||
payload: currentTarget.clientHeight + REORDER_OFFSET,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onReorderableDragEnd = () => {
|
||||
reorderDispatch({ type: 'dragEnd' });
|
||||
};
|
||||
|
||||
const extraKeyboardHandler = (e: KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (isReorderOn && keyboardMode) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
let activeDropTargetIndex = reorderableGroup.findIndex((i) => i.id === value.id);
|
||||
if (hoveredDropTarget) {
|
||||
const index = reorderableGroup.findIndex((i) => i.id === hoveredDropTarget?.id);
|
||||
if (index !== -1) activeDropTargetIndex = index;
|
||||
}
|
||||
if (e.key === keys.ARROW_LEFT || e.key === keys.ARROW_RIGHT) {
|
||||
reorderDispatch({ type: 'reset' });
|
||||
} else if (keys.ARROW_DOWN === e.key) {
|
||||
if (activeDropTargetIndex < reorderableGroup.length - 1) {
|
||||
const nextTarget = nextValidDropTarget(
|
||||
dropTargetsByOrder,
|
||||
hoveredDropTarget,
|
||||
[props.order.join(',')],
|
||||
(el) => el?.dropType === 'reorder'
|
||||
);
|
||||
onReorderableDragOver(nextTarget);
|
||||
}
|
||||
} else if (keys.ARROW_UP === e.key) {
|
||||
if (activeDropTargetIndex > 0) {
|
||||
const nextTarget = nextValidDropTarget(
|
||||
dropTargetsByOrder,
|
||||
hoveredDropTarget,
|
||||
[props.order.join(',')],
|
||||
(el) => el?.dropType === 'reorder',
|
||||
true
|
||||
);
|
||||
onReorderableDragOver(nextTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onReorderableDragOver = (target?: DropIdentifier) => {
|
||||
if (!target) {
|
||||
reorderDispatch({ type: 'reset' });
|
||||
dndDispatch({ type: 'leaveDropTarget' });
|
||||
return;
|
||||
}
|
||||
const droppingIndex = reorderableGroup.findIndex((i) => i.id === target.id);
|
||||
const draggingIndex = reorderableGroup.findIndex((i) => i.id === value?.id);
|
||||
if (draggingIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
dndDispatch({
|
||||
type: 'selectDropTarget',
|
||||
payload: {
|
||||
dropTarget: target,
|
||||
dragging: value,
|
||||
},
|
||||
});
|
||||
reorderDispatch({
|
||||
type: 'setReorderedItems',
|
||||
payload: { draggingIndex, droppingIndex, items: reorderableGroup },
|
||||
});
|
||||
};
|
||||
|
||||
const areItemsReordered = keyboardMode && isDragging && reorderedItems.length;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-test-subj={`${dataTestSubjPrefix}-reorderableDrag`}
|
||||
className={classNames({
|
||||
['domDraggable--reorderable']: isDragging,
|
||||
['domDraggable_active_keyboard--reorderable']: keyboardMode && isDragging,
|
||||
})}
|
||||
style={
|
||||
areItemsReordered
|
||||
? {
|
||||
transform: `translateY(${direction === '+' ? '-' : '+'}${reorderedItems.reduce(
|
||||
(acc, el) => acc + (el.height ?? 0) + REORDER_OFFSET,
|
||||
0
|
||||
)}px)`,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<DraggableImpl
|
||||
{...props}
|
||||
ariaDescribedBy={`${dataTestSubjPrefix}-keyboardInstructionsWithReorder`}
|
||||
extraKeyboardHandler={extraKeyboardHandler}
|
||||
onDragStart={onReorderableDragStart}
|
||||
onDragEnd={onReorderableDragEnd}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
|
@ -35,15 +35,12 @@ export const DropOverlayWrapper: React.FC<DropOverlayWrapperProps> = ({
|
|||
...otherProps
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={classnames('domDragDrop__dropOverlayWrapper', className)}
|
||||
{...(otherProps || {})}
|
||||
>
|
||||
<div className={classnames('domDroppable__overlayWrapper', className)} {...(otherProps || {})}>
|
||||
{children}
|
||||
{isVisible && (
|
||||
<div
|
||||
className="domDragDrop__dropOverlay"
|
||||
data-test-subj="domDragDrop__dropOverlay"
|
||||
className="domDroppable_overlay"
|
||||
data-test-subj="domDroppable_overlay"
|
||||
{...(overlayProps || {})}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -51,7 +51,7 @@ function getPropsForDropType(type: 'swap' | 'duplicate' | 'combine') {
|
|||
}
|
||||
}
|
||||
|
||||
const getExtraDrop = ({
|
||||
const getExtraTarget = ({
|
||||
type,
|
||||
isIncompatible,
|
||||
}: {
|
||||
|
@ -64,8 +64,8 @@ const getExtraDrop = ({
|
|||
gutterSize="s"
|
||||
justifyContent="spaceBetween"
|
||||
alignItems="center"
|
||||
className={classNames('domDragDrop__extraDrop', {
|
||||
'domDragDrop-incompatibleExtraDrop': isIncompatible,
|
||||
className={classNames('domDroppable__extraTarget', {
|
||||
'domDroppable--incompatibleExtraTarget': isIncompatible,
|
||||
})}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -88,15 +88,15 @@ const getExtraDrop = ({
|
|||
};
|
||||
|
||||
const customDropTargetsMap: Partial<{ [dropType in DropType]: React.ReactElement }> = {
|
||||
replace_duplicate_incompatible: getExtraDrop({ type: 'duplicate', isIncompatible: true }),
|
||||
duplicate_incompatible: getExtraDrop({ type: 'duplicate', isIncompatible: true }),
|
||||
swap_incompatible: getExtraDrop({ type: 'swap', isIncompatible: true }),
|
||||
replace_duplicate_compatible: getExtraDrop({ type: 'duplicate' }),
|
||||
duplicate_compatible: getExtraDrop({ type: 'duplicate' }),
|
||||
swap_compatible: getExtraDrop({ type: 'swap' }),
|
||||
field_combine: getExtraDrop({ type: 'combine' }),
|
||||
combine_compatible: getExtraDrop({ type: 'combine' }),
|
||||
combine_incompatible: getExtraDrop({ type: 'combine', isIncompatible: true }),
|
||||
replace_duplicate_incompatible: getExtraTarget({ type: 'duplicate', isIncompatible: true }),
|
||||
duplicate_incompatible: getExtraTarget({ type: 'duplicate', isIncompatible: true }),
|
||||
swap_incompatible: getExtraTarget({ type: 'swap', isIncompatible: true }),
|
||||
replace_duplicate_compatible: getExtraTarget({ type: 'duplicate' }),
|
||||
duplicate_compatible: getExtraTarget({ type: 'duplicate' }),
|
||||
swap_compatible: getExtraTarget({ type: 'swap' }),
|
||||
field_combine: getExtraTarget({ type: 'combine' }),
|
||||
combine_compatible: getExtraTarget({ type: 'combine' }),
|
||||
combine_incompatible: getExtraTarget({ type: 'combine', isIncompatible: true }),
|
||||
};
|
||||
|
||||
export const getCustomDropTarget = (dropType: DropType) => customDropTargetsMap?.[dropType] || null;
|
||||
|
@ -112,7 +112,7 @@ export const getAdditionalClassesOnEnter = (dropType?: string) => {
|
|||
'replace_duplicate_incompatible',
|
||||
].includes(dropType)
|
||||
) {
|
||||
return 'domDragDrop-isReplacing';
|
||||
return 'domDroppable--replacing';
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -128,6 +128,6 @@ export const getAdditionalClassesOnDroppable = (dropType?: string) => {
|
|||
'combine_incompatible',
|
||||
].includes(dropType)
|
||||
) {
|
||||
return 'domDragDrop-notCompatible';
|
||||
return 'domDroppable--incompatible';
|
||||
}
|
||||
};
|
||||
|
|
487
packages/kbn-dom-drag-drop/src/droppable.test.tsx
Normal file
487
packages/kbn-dom-drag-drop/src/droppable.test.tsx
Normal file
|
@ -0,0 +1,487 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, screen, act, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { Droppable } from './droppable';
|
||||
import { Draggable } from './draggable';
|
||||
import {
|
||||
dataTransfer,
|
||||
generateDragDropValue,
|
||||
renderWithDragDropContext,
|
||||
EXACT,
|
||||
} from './test_utils';
|
||||
|
||||
jest.useFakeTimers({ legacyFakeTimers: true });
|
||||
|
||||
const draggableValue = generateDragDropValue('drag_this');
|
||||
|
||||
describe('Droppable', () => {
|
||||
const onDrop = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const renderTestComponents = (propsOverrides = [{}]) => {
|
||||
const rtlRender = renderWithDragDropContext(
|
||||
<>
|
||||
<Draggable dragType="move" value={draggableValue} order={[2, 0, 0, 0]}>
|
||||
<button>Drag this</button>
|
||||
</Draggable>
|
||||
{propsOverrides.map((propOverrides, index) => (
|
||||
<Droppable
|
||||
key={index}
|
||||
order={[2, 0, index + 2, 0]}
|
||||
value={generateDragDropValue()}
|
||||
onDrop={onDrop}
|
||||
dropTypes={undefined}
|
||||
{...propOverrides}
|
||||
>
|
||||
<button>Drop here</button>
|
||||
</Droppable>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
const draggable = screen.getByTestId('domDragDrop_domDraggable_drag_this');
|
||||
const droppables = screen.queryAllByTestId('domDragDrop-domDroppable');
|
||||
const droppable = droppables[0];
|
||||
const draggableKeyboardHandler = screen.getByTestId('domDragDrop-keyboardHandler');
|
||||
const droppableContainers = screen.queryAllByTestId('domDragDropContainer');
|
||||
return {
|
||||
...rtlRender,
|
||||
draggable,
|
||||
droppableContainer: droppableContainers[0],
|
||||
startDragging: () => {
|
||||
fireEvent.dragStart(draggable, { dataTransfer });
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
drop: (droppableIndex = 0, options = {}) => {
|
||||
const dropEvent = new MouseEvent('drop', { ...options, bubbles: true });
|
||||
fireEvent(droppables[droppableIndex], dropEvent);
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
dragOver: (droppableIndex = 0, options = {}) => {
|
||||
// const dropEvent = new MouseEvent('dragOver', options);
|
||||
// fireEvent(droppables[droppableIndex], dropEvent);
|
||||
|
||||
fireEvent.dragOver(droppables[droppableIndex], options);
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
dragLeave: (droppableIndex = 0) => {
|
||||
fireEvent.dragLeave(droppables[droppableIndex]);
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
startDraggingByKeyboard: () => {
|
||||
draggableKeyboardHandler.focus();
|
||||
userEvent.keyboard('{enter}');
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
dropByKeyboard: () => {
|
||||
draggableKeyboardHandler.focus();
|
||||
userEvent.keyboard('{enter}');
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
dragOverToNextByKeyboard: () => {
|
||||
userEvent.keyboard('{arrowright}');
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
dragOverToPreviousByKeyboard: () => {
|
||||
userEvent.keyboard('{arrowleft}');
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
pressModifierKey: (key: '{Shift}' | '{Alt}' | '{Ctrl}') => {
|
||||
userEvent.keyboard(key);
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
},
|
||||
droppable,
|
||||
droppables,
|
||||
};
|
||||
};
|
||||
|
||||
test('receives additional classname when is active dropType', () => {
|
||||
const { droppable, startDragging } = renderTestComponents([{ dropTypes: ['field_add'] }]);
|
||||
|
||||
expect(droppable).toHaveClass('domDroppable', EXACT);
|
||||
startDragging();
|
||||
expect(droppable).toHaveClass('domDroppable domDroppable--active', EXACT);
|
||||
});
|
||||
test('receives additional classname when is active dropType and has custom class', () => {
|
||||
const { droppable, startDragging } = renderTestComponents([
|
||||
{
|
||||
dropTypes: ['field_add'],
|
||||
getAdditionalClassesOnEnter: () => 'customClassOnEnter',
|
||||
getAdditionalClassesOnDroppable: () => 'customClassOnActive',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(droppable).toHaveClass('domDroppable', EXACT);
|
||||
startDragging();
|
||||
expect(droppable).toHaveClass('domDroppable domDroppable--active customClassOnActive', EXACT);
|
||||
});
|
||||
test('receives additional classname when is active dropType and has custom class on enter until dragleave', () => {
|
||||
const { droppable, startDragging, dragOver, dragLeave } = renderTestComponents([
|
||||
{
|
||||
dropTypes: ['field_add'],
|
||||
getAdditionalClassesOnEnter: () => 'customClassOnEnter',
|
||||
getAdditionalClassesOnDroppable: () => 'customClassOnActive',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(droppable).toHaveClass('domDroppable', EXACT);
|
||||
startDragging();
|
||||
dragOver();
|
||||
expect(droppable).toHaveClass(
|
||||
'domDroppable domDroppable--active domDroppable--hover customClassOnActive customClassOnEnter',
|
||||
EXACT
|
||||
);
|
||||
dragLeave();
|
||||
expect(droppable).toHaveClass('domDroppable domDroppable--active customClassOnActive', EXACT);
|
||||
});
|
||||
test('receives additional classname when is active dropType and has custom class on enter until drop', () => {
|
||||
const { droppable, startDragging, dragOver, drop } = renderTestComponents([
|
||||
{
|
||||
dropTypes: ['field_add'],
|
||||
getAdditionalClassesOnEnter: () => 'customClassOnEnter',
|
||||
getAdditionalClassesOnDroppable: () => 'customClassOnActive',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(droppable).toHaveClass('domDroppable', EXACT);
|
||||
startDragging();
|
||||
dragOver();
|
||||
expect(droppable).toHaveClass(
|
||||
'domDroppable domDroppable--active domDroppable--hover customClassOnActive customClassOnEnter',
|
||||
EXACT
|
||||
);
|
||||
drop();
|
||||
expect(droppable).toHaveClass('domDroppable', EXACT);
|
||||
});
|
||||
test('gets special styling when another item is dragged if droppable doesnt have dropTypes', () => {
|
||||
const { droppable, startDragging } = renderTestComponents();
|
||||
startDragging();
|
||||
expect(droppable).toHaveClass('domDroppable domDroppable--notAllowed', EXACT);
|
||||
});
|
||||
|
||||
test('drop function is not called on dropTypes undefined', async () => {
|
||||
const { drop, startDragging } = renderTestComponents();
|
||||
startDragging();
|
||||
drop();
|
||||
expect(onDrop).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('onDrop callback is executed when dropping', async () => {
|
||||
const { startDragging, drop } = renderTestComponents([
|
||||
{
|
||||
dropTypes: ['field_add'],
|
||||
onDrop,
|
||||
},
|
||||
]);
|
||||
startDragging();
|
||||
drop();
|
||||
expect(onDrop).toBeCalledWith(
|
||||
expect.objectContaining({ humanData: expect.objectContaining({ label: 'drag_this' }) }),
|
||||
'field_add'
|
||||
);
|
||||
});
|
||||
|
||||
describe('keyboard mode', () => {
|
||||
test('drop targets get highlighted when pressing arrow keys and draggable get action class too', () => {
|
||||
const {
|
||||
droppables,
|
||||
startDraggingByKeyboard,
|
||||
dropByKeyboard,
|
||||
dragOverToNextByKeyboard,
|
||||
draggable,
|
||||
} = renderTestComponents([{ dropTypes: ['field_add'] }, { dropTypes: ['field_add'] }]);
|
||||
startDraggingByKeyboard();
|
||||
|
||||
expect(draggable).toHaveClass('domDraggable', EXACT);
|
||||
expect(droppables[0]).toHaveClass('domDroppable domDroppable--active', EXACT);
|
||||
expect(droppables[1]).toHaveClass('domDroppable domDroppable--active', EXACT);
|
||||
|
||||
dragOverToNextByKeyboard();
|
||||
expect(draggable).toHaveClass(
|
||||
'domDraggable domDraggable_dragover_keyboard--move domDraggable_dragover_keyboard--copy',
|
||||
EXACT
|
||||
);
|
||||
expect(droppables[0]).toHaveClass(
|
||||
'domDroppable domDroppable--active domDroppable--hover',
|
||||
EXACT
|
||||
);
|
||||
expect(droppables[1]).toHaveClass('domDroppable domDroppable--active', EXACT);
|
||||
dragOverToNextByKeyboard();
|
||||
expect(droppables[0]).toHaveClass('domDroppable domDroppable--active', EXACT);
|
||||
expect(droppables[1]).toHaveClass(
|
||||
'domDroppable domDroppable--active domDroppable--hover',
|
||||
EXACT
|
||||
);
|
||||
dropByKeyboard();
|
||||
expect(draggable).toHaveClass('domDraggable', EXACT);
|
||||
expect(droppables[0]).toHaveClass('domDroppable', EXACT);
|
||||
expect(droppables[1]).toHaveClass('domDroppable', EXACT);
|
||||
});
|
||||
test('executes onDrop callback when drops on drop target', () => {
|
||||
const firstDroppableOnDrop = jest.fn();
|
||||
const secondDroppableOnDrop = jest.fn();
|
||||
const { startDraggingByKeyboard, dropByKeyboard, dragOverToNextByKeyboard } =
|
||||
renderTestComponents([
|
||||
{ dropTypes: ['field_add'], onDrop: firstDroppableOnDrop },
|
||||
{ dropTypes: ['field_add'], onDrop: secondDroppableOnDrop },
|
||||
]);
|
||||
startDraggingByKeyboard();
|
||||
// goes to first target
|
||||
dragOverToNextByKeyboard();
|
||||
// goes to second target
|
||||
dragOverToNextByKeyboard();
|
||||
// drops on second target
|
||||
dropByKeyboard();
|
||||
expect(firstDroppableOnDrop).not.toBeCalled();
|
||||
expect(secondDroppableOnDrop).toHaveBeenCalledWith(draggableValue, 'field_add');
|
||||
});
|
||||
test('adds ghost to droppable when element is dragged over', async () => {
|
||||
const { startDraggingByKeyboard, droppables, draggable, dragOverToNextByKeyboard } =
|
||||
renderTestComponents([{ dropTypes: ['field_add'] }, { dropTypes: ['field_add'] }]);
|
||||
startDraggingByKeyboard();
|
||||
dragOverToNextByKeyboard();
|
||||
expect(droppables[0]).toHaveClass(
|
||||
'domDroppable domDroppable--active domDroppable--hover',
|
||||
EXACT
|
||||
);
|
||||
const domDragDropContainers = screen.queryAllByTestId('domDragDropContainer');
|
||||
|
||||
const ghostElement = within(domDragDropContainers[0]).getByText('Drag this');
|
||||
|
||||
expect(ghostElement).toHaveClass('domDraggable_ghost', EXACT);
|
||||
expect(ghostElement.textContent).toEqual(draggable.textContent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple drop targets', () => {
|
||||
test('renders extra drop targets', () => {
|
||||
const { droppables } = renderTestComponents([
|
||||
{
|
||||
dropTypes: ['move_compatible', 'duplicate_compatible', 'swap_compatible'],
|
||||
getCustomDropTarget: (dropType: string) => <div className="extraDrop">{dropType}</div>,
|
||||
},
|
||||
]);
|
||||
expect(droppables).toHaveLength(3);
|
||||
});
|
||||
test('extra drop targets appear when dragging over and disappear when hoveredDropTarget changes', () => {
|
||||
const { dragLeave, dragOver, startDragging, droppableContainer } = renderTestComponents([
|
||||
{
|
||||
dropTypes: ['move_compatible', 'duplicate_compatible', 'swap_compatible'],
|
||||
getCustomDropTarget: (dropType: string) => <div className="extraDrop">{dropType}</div>,
|
||||
},
|
||||
]);
|
||||
startDragging();
|
||||
expect(droppableContainer).toHaveClass('domDroppable__container', { exact: true });
|
||||
dragOver();
|
||||
expect(droppableContainer).toHaveClass(
|
||||
'domDroppable__container domDroppable__container-active',
|
||||
{ exact: true }
|
||||
);
|
||||
expect(screen.queryAllByTestId('domDragDropExtraTargets')[0]).toHaveClass(
|
||||
'domDroppable__extraTargets-visible'
|
||||
);
|
||||
dragLeave();
|
||||
expect(screen.queryAllByTestId('domDragDropExtraTargets')[0]).not.toHaveClass(
|
||||
'domDroppable__extraTargets-visible'
|
||||
);
|
||||
});
|
||||
|
||||
test('correct dropTarget is highlighted within drop targets with the same value and different dropTypes', () => {
|
||||
const { startDragging, dragOver, droppables, dragLeave } = renderTestComponents([
|
||||
{
|
||||
dropTypes: ['move_compatible', 'duplicate_compatible', 'swap_compatible'],
|
||||
getCustomDropTarget: (dropType: string) => <div className="extraDrop">{dropType}</div>,
|
||||
},
|
||||
]);
|
||||
startDragging();
|
||||
dragOver(1);
|
||||
expect(droppables[0]).toHaveClass('domDroppable domDroppable--active', EXACT);
|
||||
expect(droppables[1]).toHaveClass(
|
||||
'domDroppable domDroppable--active domDroppable--hover extraDrop',
|
||||
EXACT
|
||||
);
|
||||
expect(droppables[2]).toHaveClass('domDroppable domDroppable--active extraDrop', EXACT);
|
||||
dragLeave(1);
|
||||
expect(droppables[0]).toHaveClass('domDroppable domDroppable--active', EXACT);
|
||||
expect(droppables[1]).toHaveClass('domDroppable domDroppable--active extraDrop', EXACT);
|
||||
expect(droppables[2]).toHaveClass('domDroppable domDroppable--active extraDrop', EXACT);
|
||||
});
|
||||
|
||||
test('onDrop callback is executed when dropping on extra drop target', () => {
|
||||
const { startDragging, dragOver, drop } = renderTestComponents([
|
||||
{
|
||||
dropTypes: ['move_compatible', 'duplicate_compatible', 'swap_compatible'],
|
||||
onDrop,
|
||||
getCustomDropTarget: (dropType: string) => <div className="extraDrop">{dropType}</div>,
|
||||
},
|
||||
]);
|
||||
startDragging();
|
||||
dragOver();
|
||||
drop();
|
||||
expect(onDrop).toBeCalledWith(draggableValue, 'move_compatible');
|
||||
startDragging();
|
||||
dragOver(1);
|
||||
drop(1);
|
||||
expect(onDrop).toBeCalledWith(draggableValue, 'duplicate_compatible');
|
||||
startDragging();
|
||||
dragOver(2);
|
||||
drop(2);
|
||||
expect(onDrop).toBeCalledWith(draggableValue, 'swap_compatible');
|
||||
});
|
||||
test('pressing Alt or Shift when dragging over the main drop target sets extra drop target as active', () => {
|
||||
const { startDragging, dragOver, drop } = renderTestComponents([
|
||||
{
|
||||
dropTypes: ['move_compatible', 'duplicate_compatible', 'swap_compatible'],
|
||||
onDrop,
|
||||
getCustomDropTarget: (dropType: string) => <div className="extraDrop">{dropType}</div>,
|
||||
},
|
||||
]);
|
||||
startDragging();
|
||||
dragOver(0, { altKey: true });
|
||||
drop(0, { altKey: true });
|
||||
expect(onDrop).toBeCalledWith(draggableValue, 'duplicate_compatible');
|
||||
|
||||
startDragging();
|
||||
dragOver(0, { shiftKey: true });
|
||||
drop(0, { shiftKey: true });
|
||||
expect(onDrop).toBeCalledWith(draggableValue, 'swap_compatible');
|
||||
});
|
||||
test('pressing Alt or Shift when dragging over the extra drop target does nothing', () => {
|
||||
const { startDragging, dragOver, drop } = renderTestComponents([
|
||||
{
|
||||
dropTypes: ['move_compatible', 'duplicate_compatible', 'swap_compatible'],
|
||||
onDrop,
|
||||
getCustomDropTarget: (dropType: string) => <div className="extraDrop">{dropType}</div>,
|
||||
},
|
||||
]);
|
||||
startDragging();
|
||||
dragOver(1, { shiftKey: true });
|
||||
drop(1, { shiftKey: true });
|
||||
expect(onDrop).toBeCalledWith(draggableValue, 'duplicate_compatible');
|
||||
});
|
||||
describe('keyboard mode', () => {
|
||||
test('user can go through all the drop targets ', () => {
|
||||
const { startDraggingByKeyboard, dragOverToNextByKeyboard, droppables, pressModifierKey } =
|
||||
renderTestComponents([
|
||||
{
|
||||
dropTypes: ['move_compatible', 'duplicate_compatible', 'swap_compatible'],
|
||||
getCustomDropTarget: (dropType: string) => (
|
||||
<div className="extraDrop">{dropType}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
dropTypes: ['move_compatible'],
|
||||
getCustomDropTarget: (dropType: string) => (
|
||||
<div className="extraDrop">{dropType}</div>
|
||||
),
|
||||
},
|
||||
]);
|
||||
startDraggingByKeyboard();
|
||||
dragOverToNextByKeyboard();
|
||||
expect(droppables[0]).toHaveClass('domDroppable--hover');
|
||||
pressModifierKey('{Alt}');
|
||||
expect(droppables[1]).toHaveClass('domDroppable--hover');
|
||||
pressModifierKey('{Shift}');
|
||||
expect(droppables[2]).toHaveClass('domDroppable--hover');
|
||||
dragOverToNextByKeyboard();
|
||||
expect(droppables[3]).toHaveClass('domDroppable--hover');
|
||||
dragOverToNextByKeyboard();
|
||||
// we circled back to the draggable (no drop target is selected)
|
||||
dragOverToNextByKeyboard();
|
||||
expect(droppables[0]).toHaveClass('domDroppable--hover');
|
||||
});
|
||||
test('user can go through all the drop targets in reverse direction', () => {
|
||||
const {
|
||||
startDraggingByKeyboard,
|
||||
dragOverToPreviousByKeyboard,
|
||||
droppables,
|
||||
pressModifierKey,
|
||||
} = renderTestComponents([
|
||||
{
|
||||
dropTypes: ['move_compatible', 'duplicate_compatible', 'swap_compatible'],
|
||||
getCustomDropTarget: (dropType: string) => <div className="extraDrop">{dropType}</div>,
|
||||
},
|
||||
{
|
||||
dropTypes: ['move_compatible'],
|
||||
getCustomDropTarget: (dropType: string) => <div className="extraDrop">{dropType}</div>,
|
||||
},
|
||||
]);
|
||||
startDraggingByKeyboard();
|
||||
dragOverToPreviousByKeyboard();
|
||||
expect(droppables[3]).toHaveClass('domDroppable--hover');
|
||||
dragOverToPreviousByKeyboard();
|
||||
expect(droppables[0]).toHaveClass('domDroppable--hover');
|
||||
pressModifierKey('{Alt}');
|
||||
expect(droppables[1]).toHaveClass('domDroppable--hover');
|
||||
pressModifierKey('{Shift}');
|
||||
expect(droppables[2]).toHaveClass('domDroppable--hover');
|
||||
dragOverToPreviousByKeyboard();
|
||||
// we circled back to the draggable (no drop target is selected)
|
||||
dragOverToPreviousByKeyboard();
|
||||
expect(droppables[3]).toHaveClass('domDroppable--hover');
|
||||
});
|
||||
test('user can drop on extra drop targets', () => {
|
||||
const {
|
||||
startDraggingByKeyboard,
|
||||
dragOverToNextByKeyboard,
|
||||
dropByKeyboard,
|
||||
pressModifierKey,
|
||||
} = renderTestComponents([
|
||||
{
|
||||
dropTypes: ['move_compatible', 'duplicate_compatible', 'swap_compatible'],
|
||||
onDrop,
|
||||
getCustomDropTarget: (dropType: string) => <div className="extraDrop">{dropType}</div>,
|
||||
},
|
||||
]);
|
||||
startDraggingByKeyboard();
|
||||
dragOverToNextByKeyboard();
|
||||
dropByKeyboard();
|
||||
expect(onDrop).toHaveBeenCalledWith(draggableValue, 'move_compatible');
|
||||
onDrop.mockClear();
|
||||
|
||||
startDraggingByKeyboard();
|
||||
dragOverToNextByKeyboard();
|
||||
pressModifierKey('{Alt}');
|
||||
dropByKeyboard();
|
||||
expect(onDrop).toHaveBeenCalledWith(draggableValue, 'duplicate_compatible');
|
||||
onDrop.mockClear();
|
||||
|
||||
startDraggingByKeyboard();
|
||||
dragOverToNextByKeyboard();
|
||||
pressModifierKey('{Shift}');
|
||||
dropByKeyboard();
|
||||
expect(onDrop).toHaveBeenCalledWith(draggableValue, 'swap_compatible');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
481
packages/kbn-dom-drag-drop/src/droppable.tsx
Normal file
481
packages/kbn-dom-drag-drop/src/droppable.tsx
Normal file
|
@ -0,0 +1,481 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useContext, useCallback, useEffect, memo, useState, useRef } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
|
||||
import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect';
|
||||
import {
|
||||
DragDropIdentifier,
|
||||
ReorderContext,
|
||||
DropHandler,
|
||||
Ghost,
|
||||
DragDropAction,
|
||||
DragContextState,
|
||||
useDragDropContext,
|
||||
} from './providers';
|
||||
import { DropType } from './types';
|
||||
import './sass/droppable.scss';
|
||||
|
||||
type DroppableEvent = React.DragEvent<HTMLElement>;
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
/**
|
||||
* The base props to the Droppable component.
|
||||
*/
|
||||
export interface DroppableProps {
|
||||
/**
|
||||
* The CSS class(es) for the root element.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* The event handler that fires when an item
|
||||
* is dropped onto this Droppable component.
|
||||
*/
|
||||
onDrop?: DropHandler;
|
||||
/**
|
||||
* The value associated with this item.
|
||||
*/
|
||||
value: DragDropIdentifier;
|
||||
|
||||
/**
|
||||
* The React element which will be passed the draggable handlers
|
||||
*/
|
||||
children: ReactElement;
|
||||
|
||||
/**
|
||||
* Disable any drag & drop behaviour
|
||||
*/
|
||||
isDisabled?: 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 the type of drop targets - when undefined, the currently dragged item
|
||||
* cannot be dropped onto this component.
|
||||
*/
|
||||
dropTypes?: DropType[];
|
||||
/**
|
||||
* Order for keyboard dragging. This takes an array of numbers which will be used to order hierarchically
|
||||
*/
|
||||
order: number[];
|
||||
/**
|
||||
* Extra drop targets by dropType
|
||||
*/
|
||||
getCustomDropTarget?: (dropType: DropType) => ReactElement | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The props for a non-draggable instance of that component.
|
||||
*/
|
||||
interface DropsInnerProps extends DroppableProps {
|
||||
dndState: DragContextState;
|
||||
dndDispatch: React.Dispatch<DragDropAction>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Droppable component
|
||||
* @param props
|
||||
* @constructor
|
||||
*/
|
||||
export const Droppable = (props: DroppableProps) => {
|
||||
const [dndState, dndDispatch] = useDragDropContext();
|
||||
|
||||
if (props.isDisabled) {
|
||||
return props.children;
|
||||
}
|
||||
|
||||
const { dropTypes, reorderableGroup } = props;
|
||||
|
||||
const dropProps = {
|
||||
...props,
|
||||
dndState,
|
||||
dndDispatch,
|
||||
};
|
||||
if (reorderableGroup && reorderableGroup.length > 1 && dropTypes?.[0] === 'reorder') {
|
||||
return <ReorderableDroppableImpl {...dropProps} reorderableGroup={reorderableGroup} />;
|
||||
}
|
||||
return <DroppableImpl {...dropProps} />;
|
||||
};
|
||||
|
||||
const DroppableImpl = memo(function DroppableImpl(props: DropsInnerProps) {
|
||||
const {
|
||||
dataTestSubj,
|
||||
className,
|
||||
onDrop,
|
||||
value,
|
||||
children,
|
||||
dndState,
|
||||
dndDispatch,
|
||||
dropTypes,
|
||||
order,
|
||||
getAdditionalClassesOnEnter,
|
||||
getAdditionalClassesOnDroppable,
|
||||
getCustomDropTarget,
|
||||
} = props;
|
||||
|
||||
const { dragging, hoveredDropTarget, dataTestSubjPrefix, keyboardMode } = dndState;
|
||||
|
||||
const [isInZone, setIsInZone] = useState(false);
|
||||
const mainTargetRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useShallowCompareEffect(() => {
|
||||
if (dropTypes && dropTypes?.[0] && onDrop && keyboardMode) {
|
||||
dndDispatch({
|
||||
type: 'registerDropTargets',
|
||||
payload: dropTypes.reduce(
|
||||
(acc, dropType, index) => ({
|
||||
...acc,
|
||||
[[...order, index].join(',')]: { ...value, onDrop, dropType },
|
||||
}),
|
||||
{}
|
||||
),
|
||||
});
|
||||
}
|
||||
}, [order, dndDispatch, dropTypes, keyboardMode]);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
if (hoveredDropTarget && hoveredDropTarget.id !== value.id) {
|
||||
setIsInZone(false);
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (!hoveredDropTarget && isMounted) {
|
||||
setIsInZone(false);
|
||||
}
|
||||
}, 1000);
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [hoveredDropTarget, setIsInZone, value.id]);
|
||||
|
||||
const dragEnter = useCallback(() => {
|
||||
if (!isInZone) {
|
||||
setIsInZone(true);
|
||||
}
|
||||
}, [isInZone]);
|
||||
|
||||
const getModifiedDropType = useCallback(
|
||||
(e: DroppableEvent, dropType: DropType) => {
|
||||
if (!dropTypes || dropTypes.length <= 1) {
|
||||
return dropType;
|
||||
}
|
||||
const dropIndex = dropTypes.indexOf(dropType);
|
||||
if (dropIndex > 0) {
|
||||
return dropType;
|
||||
} else if (dropIndex === 0) {
|
||||
if (e.altKey && dropTypes[1]) {
|
||||
return dropTypes[1];
|
||||
} else if (e.shiftKey && dropTypes[2]) {
|
||||
return dropTypes[2];
|
||||
} else if (e.ctrlKey && (dropTypes.length > 3 ? dropTypes[3] : dropTypes[1])) {
|
||||
return dropTypes.length > 3 ? dropTypes[3] : dropTypes[1];
|
||||
}
|
||||
}
|
||||
return dropType;
|
||||
},
|
||||
[dropTypes]
|
||||
);
|
||||
|
||||
const dragOver = (e: DroppableEvent, dropType: DropType) => {
|
||||
e.preventDefault();
|
||||
if (!dragging || !onDrop) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modifiedDropType = getModifiedDropType(e, dropType);
|
||||
|
||||
const isSelectedDropTarget = !!(
|
||||
hoveredDropTarget?.id === value.id && hoveredDropTarget?.dropType === modifiedDropType
|
||||
);
|
||||
// An optimization to prevent a bunch of React churn.
|
||||
if (!isSelectedDropTarget) {
|
||||
dndDispatch({
|
||||
type: 'selectDropTarget',
|
||||
payload: {
|
||||
dropTarget: { ...value, dropType: modifiedDropType, onDrop },
|
||||
dragging,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const dragLeave = useCallback(() => {
|
||||
dndDispatch({ type: 'leaveDropTarget' });
|
||||
}, [dndDispatch]);
|
||||
|
||||
const drop = useCallback(
|
||||
(e: DroppableEvent, dropType: DropType) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsInZone(false);
|
||||
if (onDrop && dragging) {
|
||||
const modifiedDropType = getModifiedDropType(e, dropType);
|
||||
onDrop(dragging, modifiedDropType);
|
||||
setTimeout(() => {
|
||||
dndDispatch({
|
||||
type: 'dropToTarget',
|
||||
payload: {
|
||||
dragging,
|
||||
dropTarget: { ...value, dropType: modifiedDropType, onDrop },
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
dndDispatch({ type: 'resetState' });
|
||||
},
|
||||
[dndDispatch, onDrop, dragging, getModifiedDropType, value]
|
||||
);
|
||||
const getProps = (dropType?: DropType, dropChildren?: ReactElement) => {
|
||||
const isSelectedDropTarget = Boolean(
|
||||
hoveredDropTarget?.id === value.id && dropType === hoveredDropTarget?.dropType
|
||||
);
|
||||
|
||||
return {
|
||||
'data-test-subj': dataTestSubj || `${dataTestSubjPrefix}-domDroppable`,
|
||||
className: getClasses(dropType, dropChildren),
|
||||
onDragEnter: dragEnter,
|
||||
onDragLeave: dragLeave,
|
||||
onDragOver: dropType
|
||||
? (e: DroppableEvent) => {
|
||||
dragOver(e, dropType);
|
||||
}
|
||||
: noop,
|
||||
onDrop: dropType
|
||||
? (e: DroppableEvent) => {
|
||||
drop(e, dropType);
|
||||
}
|
||||
: noop,
|
||||
ghost:
|
||||
(isSelectedDropTarget && dropType !== 'reorder' && dragging?.ghost && dragging.ghost) ||
|
||||
undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const getClasses = (dropType?: DropType, dropChildren = children) => {
|
||||
const isSelectedDropTarget = Boolean(
|
||||
hoveredDropTarget?.id === value.id && dropType === hoveredDropTarget?.dropType
|
||||
);
|
||||
const classesOnDroppable = getAdditionalClassesOnDroppable?.(dropType);
|
||||
|
||||
const classes = classNames(
|
||||
'domDroppable',
|
||||
{
|
||||
'domDroppable--active': dragging && dropType,
|
||||
'domDroppable--hover': dropType && isSelectedDropTarget,
|
||||
'domDroppable--notAllowed':
|
||||
dragging && (!dropTypes || !dropTypes.length) && value.id !== dragging.id,
|
||||
},
|
||||
classesOnDroppable && { [classesOnDroppable]: dragging && dropType }
|
||||
);
|
||||
return classNames(classes, className, dropChildren.props.className);
|
||||
};
|
||||
|
||||
const getMainTargetClasses = () => {
|
||||
const classesOnEnter = getAdditionalClassesOnEnter?.(hoveredDropTarget?.dropType);
|
||||
return classNames(classesOnEnter && { [classesOnEnter]: hoveredDropTarget?.id === value.id });
|
||||
};
|
||||
|
||||
const mainTargetProps = getProps(dropTypes && dropTypes[0]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-test-subj={`${dataTestSubjPrefix}Container`}
|
||||
className={classNames('domDroppable__container', {
|
||||
'domDroppable__container-active': isInZone || hoveredDropTarget?.id === value.id,
|
||||
})}
|
||||
onDragEnter={dragEnter}
|
||||
ref={mainTargetRef}
|
||||
>
|
||||
<SingleDropInner
|
||||
{...mainTargetProps}
|
||||
className={classNames(mainTargetProps.className, getMainTargetClasses())}
|
||||
>
|
||||
{children}
|
||||
</SingleDropInner>
|
||||
{dropTypes && dropTypes.length > 1 && (
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
direction="column"
|
||||
data-test-subj={`${dataTestSubjPrefix}ExtraTargets`}
|
||||
className={classNames('domDroppable__extraTargets', {
|
||||
'domDroppable__extraTargets-visible': isInZone || hoveredDropTarget?.id === value.id,
|
||||
})}
|
||||
>
|
||||
{dropTypes.slice(1).map((dropType) => {
|
||||
const dropChildren = getCustomDropTarget?.(dropType);
|
||||
return dropChildren ? (
|
||||
<EuiFlexItem key={dropType} className="domDroppable__extraDropWrapper">
|
||||
<SingleDropInner {...getProps(dropType, dropChildren)}>
|
||||
{dropChildren}
|
||||
</SingleDropInner>
|
||||
</EuiFlexItem>
|
||||
) : null;
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const SingleDropInner = ({
|
||||
ghost,
|
||||
children,
|
||||
...rest
|
||||
}: {
|
||||
ghost?: Ghost;
|
||||
children: ReactElement;
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{React.cloneElement(children, rest)}
|
||||
{ghost
|
||||
? React.cloneElement(ghost.children, {
|
||||
className: classNames(
|
||||
ghost.children.props.className,
|
||||
ghost.className,
|
||||
'domDraggable_ghost'
|
||||
),
|
||||
style: ghost.style,
|
||||
})
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ReorderableDroppableImpl = memo(function ReorderableDroppableImpl(
|
||||
props: DropsInnerProps & { reorderableGroup: Array<{ id: string }> }
|
||||
) {
|
||||
const { onDrop, value, dndState, dndDispatch, reorderableGroup, className } = props;
|
||||
|
||||
const { dragging, dataTestSubjPrefix, hoveredDropTarget } = dndState;
|
||||
const currentIndex = reorderableGroup.findIndex((i) => i.id === value.id);
|
||||
|
||||
const [{ isReorderOn, reorderedItems, draggingHeight, direction }, reorderDispatch] =
|
||||
useContext(ReorderContext);
|
||||
|
||||
const heightRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isReordered =
|
||||
isReorderOn && reorderedItems.some((el) => el.id === value.id) && reorderedItems.length;
|
||||
|
||||
useEffect(() => {
|
||||
if (isReordered && heightRef.current?.clientHeight) {
|
||||
reorderDispatch({
|
||||
type: 'registerReorderedItemHeight',
|
||||
payload: { id: value.id, height: heightRef.current.clientHeight },
|
||||
});
|
||||
}
|
||||
}, [isReordered, reorderDispatch, value.id]);
|
||||
|
||||
const onReorderableDragOver = (e: DroppableEvent) => {
|
||||
e.preventDefault();
|
||||
// An optimization to prevent a bunch of React churn.
|
||||
if (hoveredDropTarget?.id !== value?.id && onDrop) {
|
||||
const draggingIndex = reorderableGroup.findIndex((i) => i.id === dragging?.id);
|
||||
if (!dragging || draggingIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const droppingIndex = currentIndex;
|
||||
if (draggingIndex === droppingIndex) {
|
||||
reorderDispatch({ type: 'reset' });
|
||||
}
|
||||
|
||||
reorderDispatch({
|
||||
type: 'setReorderedItems',
|
||||
payload: { draggingIndex, droppingIndex, items: reorderableGroup },
|
||||
});
|
||||
dndDispatch({
|
||||
type: 'selectDropTarget',
|
||||
payload: {
|
||||
dropTarget: { ...value, dropType: 'reorder', onDrop },
|
||||
dragging,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onReorderableDrop = (e: DroppableEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (onDrop && dragging) {
|
||||
onDrop(dragging, 'reorder');
|
||||
// setTimeout ensures it will run after dragEnd messaging
|
||||
setTimeout(() => {
|
||||
dndDispatch({
|
||||
type: 'dropToTarget',
|
||||
payload: {
|
||||
dragging,
|
||||
dropTarget: { ...value, dropType: 'reorder', onDrop },
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
dndDispatch({ type: 'resetState' });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={
|
||||
reorderedItems.some((i) => i.id === value.id)
|
||||
? {
|
||||
transform: `translateY(${direction}${draggingHeight}px)`,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
ref={heightRef}
|
||||
data-test-subj={`${dataTestSubjPrefix}-translatableDrop`}
|
||||
className="domDroppable--translatable"
|
||||
>
|
||||
<DroppableImpl
|
||||
{...props}
|
||||
className={classNames(className, {
|
||||
['domDroppable__overlayWrapper']: dragging,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-test-subj={`${dataTestSubjPrefix}-reorderableDropLayer`}
|
||||
className={classNames({
|
||||
['domDroppable--reorderable']: dragging,
|
||||
})}
|
||||
onDrop={onReorderableDrop}
|
||||
onDragOver={onReorderableDragOver}
|
||||
onDragLeave={() => {
|
||||
dndDispatch({ type: 'leaveDropTarget' });
|
||||
reorderDispatch({ type: 'reset' });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
|
@ -8,5 +8,6 @@
|
|||
|
||||
export * from './types';
|
||||
export * from './providers';
|
||||
export * from './drag_drop';
|
||||
export * from './draggable';
|
||||
export * from './droppable';
|
||||
export { DropOverlayWrapper, type DropOverlayWrapperProps } from './drop_overlay_wrapper';
|
||||
|
|
|
@ -21,9 +21,9 @@ import {
|
|||
import { DEFAULT_DATA_TEST_SUBJ } from '../constants';
|
||||
import { announce } from './announcements';
|
||||
|
||||
const initialState = {
|
||||
const defaultState = {
|
||||
dragging: undefined,
|
||||
activeDropTarget: undefined,
|
||||
hoveredDropTarget: undefined,
|
||||
keyboardMode: false,
|
||||
dropTargetsByOrder: {},
|
||||
dataTestSubjPrefix: DEFAULT_DATA_TEST_SUBJ,
|
||||
|
@ -33,7 +33,7 @@ const initialState = {
|
|||
*
|
||||
* const [ state, dispatch ] = useDragDropContext();
|
||||
*/
|
||||
const DragContext = React.createContext<DragContextValue>([initialState, () => {}]);
|
||||
const DragContext = React.createContext<DragContextValue>([defaultState, () => {}]);
|
||||
|
||||
export function useDragDropContext() {
|
||||
const context = React.useContext(DragContext);
|
||||
|
@ -127,7 +127,7 @@ const dragDropReducer = (state: DragContextState, action: DragDropAction) => {
|
|||
dropTargetsByOrder: undefined,
|
||||
dragging: undefined,
|
||||
keyboardMode: false,
|
||||
activeDropTarget: undefined,
|
||||
hoveredDropTarget: undefined,
|
||||
};
|
||||
case 'registerDropTargets':
|
||||
return {
|
||||
|
@ -143,17 +143,17 @@ const dragDropReducer = (state: DragContextState, action: DragDropAction) => {
|
|||
dropTargetsByOrder: undefined,
|
||||
dragging: undefined,
|
||||
keyboardMode: false,
|
||||
activeDropTarget: undefined,
|
||||
hoveredDropTarget: undefined,
|
||||
};
|
||||
case 'leaveDropTarget':
|
||||
return {
|
||||
...state,
|
||||
activeDropTarget: undefined,
|
||||
hoveredDropTarget: undefined,
|
||||
};
|
||||
case 'selectDropTarget':
|
||||
return {
|
||||
...state,
|
||||
activeDropTarget: action.payload.dropTarget,
|
||||
hoveredDropTarget: action.payload.dropTarget,
|
||||
};
|
||||
case 'startDragging':
|
||||
return {
|
||||
|
@ -222,23 +222,25 @@ const useA11yMiddleware = () => {
|
|||
|
||||
export function RootDragDropProvider({
|
||||
children,
|
||||
dataTestSubj = DEFAULT_DATA_TEST_SUBJ,
|
||||
customMiddleware,
|
||||
initialState = {},
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
dataTestSubj?: string;
|
||||
customMiddleware?: CustomMiddleware;
|
||||
initialState?: Partial<DragContextState>;
|
||||
}) {
|
||||
const { a11yMessage, a11yMiddleware } = useA11yMiddleware();
|
||||
const middlewareFns = React.useMemo(() => {
|
||||
return customMiddleware ? [customMiddleware, a11yMiddleware] : [a11yMiddleware];
|
||||
}, [customMiddleware, a11yMiddleware]);
|
||||
|
||||
const dataTestSubj = initialState.dataTestSubjPrefix || DEFAULT_DATA_TEST_SUBJ;
|
||||
|
||||
const [state, dispatch] = useReducerWithMiddleware(
|
||||
dragDropReducer,
|
||||
{
|
||||
...defaultState,
|
||||
...initialState,
|
||||
dataTestSubjPrefix: dataTestSubj,
|
||||
},
|
||||
middlewareFns
|
||||
);
|
||||
|
@ -269,7 +271,7 @@ export function RootDragDropProvider({
|
|||
|
||||
export function nextValidDropTarget(
|
||||
dropTargetsByOrder: RegisteredDropTargets,
|
||||
activeDropTarget: DropIdentifier | undefined,
|
||||
hoveredDropTarget: DropIdentifier | undefined,
|
||||
draggingOrder: [string],
|
||||
filterElements: (el: DragDropIdentifier) => boolean = () => true,
|
||||
reverse = false
|
||||
|
@ -278,18 +280,13 @@ export function nextValidDropTarget(
|
|||
return;
|
||||
}
|
||||
|
||||
const filteredTargets: Array<[string, DropIdentifier | undefined]> = Object.entries(
|
||||
dropTargetsByOrder
|
||||
).filter(([, dropTarget]) => {
|
||||
return dropTarget && filterElements(dropTarget);
|
||||
const filteredTargets = Object.entries(dropTargetsByOrder).filter(([order, dropTarget]) => {
|
||||
return dropTarget && order !== draggingOrder[0] && filterElements(dropTarget);
|
||||
});
|
||||
|
||||
// filter out secondary targets
|
||||
const uniqueIdTargets = filteredTargets.reduce(
|
||||
(
|
||||
acc: Array<[string, DropIdentifier | undefined]>,
|
||||
current: [string, DropIdentifier | undefined]
|
||||
) => {
|
||||
// filter out secondary targets and targets with the same id as the dragging element
|
||||
const uniqueIdTargets = filteredTargets.reduce<Array<[string, DropIdentifier]>>(
|
||||
(acc, current) => {
|
||||
const [, currentDropTarget] = current;
|
||||
if (!currentDropTarget) {
|
||||
return acc;
|
||||
|
@ -311,7 +308,7 @@ export function nextValidDropTarget(
|
|||
});
|
||||
|
||||
let currentActiveDropIndex = nextDropTargets.findIndex(
|
||||
([_, dropTarget]) => dropTarget?.id === activeDropTarget?.id
|
||||
([, dropTarget]) => typeof dropTarget === 'object' && dropTarget?.id === hoveredDropTarget?.id
|
||||
);
|
||||
|
||||
if (currentActiveDropIndex === -1) {
|
||||
|
|
|
@ -65,6 +65,10 @@ interface ResetAction {
|
|||
type: 'reset';
|
||||
}
|
||||
|
||||
interface DragEndAction {
|
||||
type: 'dragEnd';
|
||||
}
|
||||
|
||||
interface RegisterDraggingItemHeightAction {
|
||||
type: 'registerDraggingItemHeight';
|
||||
payload: number;
|
||||
|
@ -90,6 +94,7 @@ interface SetReorderedItemsAction {
|
|||
}
|
||||
|
||||
type ReorderAction =
|
||||
| DragEndAction
|
||||
| ResetAction
|
||||
| RegisterDraggingItemHeightAction
|
||||
| RegisterReorderedItemHeightAction
|
||||
|
@ -98,6 +103,8 @@ type ReorderAction =
|
|||
|
||||
const reorderReducer = (state: ReorderState, action: ReorderAction) => {
|
||||
switch (action.type) {
|
||||
case 'dragEnd':
|
||||
return { ...state, reorderedItems: [], isReorderOn: false };
|
||||
case 'reset':
|
||||
return { ...state, reorderedItems: [] };
|
||||
case 'registerDraggingItemHeight':
|
||||
|
@ -110,7 +117,11 @@ const reorderReducer = (state: ReorderState, action: ReorderAction) => {
|
|||
),
|
||||
};
|
||||
case 'setIsReorderOn':
|
||||
return { ...state, isReorderOn: action.payload };
|
||||
return {
|
||||
...state,
|
||||
isReorderOn: action.payload,
|
||||
reorderedItems: action.payload ? state.reorderedItems : [],
|
||||
};
|
||||
case 'setReorderedItems':
|
||||
const { items, draggingIndex, droppingIndex } = action.payload;
|
||||
return draggingIndex < droppingIndex
|
||||
|
@ -146,7 +157,7 @@ export function ReorderProvider({
|
|||
return (
|
||||
<div
|
||||
data-test-subj={`${dataTestSubj}-reorderableGroup`}
|
||||
className={classNames(className, {
|
||||
className={classNames(className, 'domDragDrop-group', {
|
||||
'domDragDrop-isActiveGroup': state.isReorderOn && React.Children.count(children) > 1,
|
||||
})}
|
||||
>
|
||||
|
|
|
@ -22,6 +22,7 @@ export interface HumanData {
|
|||
|
||||
export interface Ghost {
|
||||
children: React.ReactElement;
|
||||
className?: string;
|
||||
style: React.CSSProperties;
|
||||
}
|
||||
|
||||
|
@ -56,7 +57,7 @@ export type DropIdentifier = DragDropIdentifier & {
|
|||
*/
|
||||
export type DropHandler = (dropped: DragDropIdentifier, dropType?: DropType) => void;
|
||||
|
||||
export type RegisteredDropTargets = Record<string, DropIdentifier | undefined> | undefined;
|
||||
export type RegisteredDropTargets = Record<string, DropIdentifier> | undefined;
|
||||
|
||||
export interface DragContextState {
|
||||
/**
|
||||
|
@ -70,7 +71,7 @@ export interface DragContextState {
|
|||
/**
|
||||
* currently selected drop target
|
||||
*/
|
||||
activeDropTarget?: DropIdentifier;
|
||||
hoveredDropTarget?: DropIdentifier;
|
||||
/**
|
||||
* currently registered drop targets
|
||||
*/
|
||||
|
|
|
@ -8,16 +8,15 @@ $domDragDropZLevel3: 3;
|
|||
// from mixins
|
||||
|
||||
// sass-lint:disable-block indentation, no-color-keywords
|
||||
|
||||
// Static styles for a draggable item
|
||||
@mixin mixinDomDraggable {
|
||||
@include euiSlightShadow;
|
||||
background: $euiColorEmptyShade;
|
||||
cursor: grab;
|
||||
@mixin mixinDomDragDrop {
|
||||
transition: $euiAnimSpeedFast ease-in-out;
|
||||
transition-property: background-color, border-color, opacity;
|
||||
z-index: $domDragDropZLevel1;
|
||||
border-radius: $euiBorderRadius;
|
||||
}
|
||||
|
||||
// Hovering state for drag item and drop area
|
||||
@mixin mixinDomDragDropHover {
|
||||
@mixin mixinDomDraggableHover {
|
||||
&:hover {
|
||||
transform: translateX($euiSizeXS);
|
||||
transition: transform $euiAnimSpeedSlow ease-out;
|
||||
|
@ -48,7 +47,7 @@ $domDragDropZLevel3: 3;
|
|||
}
|
||||
|
||||
// Style for drop area while hovering with item
|
||||
@mixin mixinDomDroppableActiveHover($borderWidth: $euiBorderWidthThin) {
|
||||
@mixin mixinDomDroppableHover($borderWidth: $euiBorderWidthThin) {
|
||||
background-color: transparentize($euiColorVis0, .75) !important;
|
||||
&:before {
|
||||
border-color: $euiColorVis0 !important;
|
||||
|
|
44
packages/kbn-dom-drag-drop/src/sass/draggable.scss
Normal file
44
packages/kbn-dom-drag-drop/src/sass/draggable.scss
Normal file
|
@ -0,0 +1,44 @@
|
|||
@import './drag_drop_mixins';
|
||||
|
||||
// Draggable item
|
||||
.domDraggable {
|
||||
cursor: grab;
|
||||
@include mixinDomDragDrop;
|
||||
@include mixinDomDraggableHover;
|
||||
|
||||
// Include a possible nested button like when using FieldButton
|
||||
& .kbnFieldButton__button,
|
||||
& .euiLink {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@include euiFocusRing;
|
||||
}
|
||||
}
|
||||
|
||||
.domDraggable--reorderable {
|
||||
transform: translateY(0);
|
||||
transition: transform $euiAnimSpeedFast ease-in-out;
|
||||
position: relative;
|
||||
z-index: $domDragDropZLevel1;
|
||||
}
|
||||
|
||||
.domDraggable__keyboardHandler {
|
||||
top: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: $euiBorderRadius;
|
||||
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
@include euiFocusRing;
|
||||
pointer-events: none;
|
||||
z-index: $domDragDropZLevel2;
|
||||
}
|
||||
}
|
||||
|
||||
.domDraggable_active--move, .domDraggable_dragover_keyboard--move {
|
||||
opacity: 0;
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
@import './drag_drop_mixins';
|
||||
|
||||
.domDragDrop {
|
||||
transition: $euiAnimSpeedFast ease-in-out;
|
||||
transition-property: background-color, border-color, opacity;
|
||||
z-index: $domDragDropZLevel1;
|
||||
// Drop area
|
||||
.domDroppable {
|
||||
@include mixinDomDragDrop;
|
||||
&:not(.domDroppable__overlayWrapper) {
|
||||
@include mixinDomDroppable;
|
||||
}
|
||||
}
|
||||
|
||||
.domDragDrop_ghost {
|
||||
@include mixinDomDraggable;
|
||||
border: $euiBorderWidthThin dashed $euiBorderColor;
|
||||
.domDraggable_ghost {
|
||||
position: absolute !important; // sass-lint:disable-line no-important
|
||||
margin: 0 !important; // sass-lint:disable-line no-important
|
||||
top: 0;
|
||||
|
@ -22,30 +22,9 @@
|
|||
outline-style: auto; // Chrome
|
||||
}
|
||||
|
||||
// Draggable item
|
||||
.domDragDrop-isDraggable {
|
||||
@include mixinDomDraggable;
|
||||
@include mixinDomDragDropHover;
|
||||
|
||||
// Include a possible nested button like when using FieldButton
|
||||
& .kbnFieldButton__button,
|
||||
& .euiLink {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@include euiFocusRing;
|
||||
}
|
||||
}
|
||||
|
||||
// Drop area
|
||||
.domDragDrop-isDroppable:not(.domDragDrop__dropOverlayWrapper) {
|
||||
@include mixinDomDroppable;
|
||||
}
|
||||
|
||||
// Drop area when there's an item being dragged
|
||||
.domDragDrop-isDropTarget {
|
||||
&:not(.domDragDrop__dropOverlayWrapper) {
|
||||
.domDroppable--active {
|
||||
&:not(.domDroppable__overlayWrapper) {
|
||||
@include mixinDomDroppable;
|
||||
@include mixinDomDroppableActive;
|
||||
}
|
||||
|
@ -56,27 +35,27 @@
|
|||
}
|
||||
|
||||
// Drop area while hovering with item
|
||||
.domDragDrop-isActiveDropTarget:not(.domDragDrop__dropOverlayWrapper) {
|
||||
.domDroppable--hover:not(.domDroppable__overlayWrapper) {
|
||||
z-index: $domDragDropZLevel3;
|
||||
@include mixinDomDroppableActiveHover;
|
||||
@include mixinDomDroppableHover;
|
||||
}
|
||||
|
||||
// Drop area that is not allowed for current item
|
||||
.domDragDrop-isNotDroppable {
|
||||
.domDroppable--notAllowed {
|
||||
@include mixinDomDroppableNotAllowed;
|
||||
}
|
||||
|
||||
// Drop area will be replacing existing content
|
||||
.domDragDrop-isReplacing {
|
||||
.domDroppable--replacing {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.domDragDrop-notCompatible:not(.domDragDrop__dropOverlayWrapper) {
|
||||
.domDroppable--incompatible:not(.domDroppable__overlayWrapper) {
|
||||
background-color: $euiColorHighlight !important;
|
||||
&:before {
|
||||
border: $euiBorderWidthThin dashed $euiColorVis5 !important;
|
||||
}
|
||||
&.domDragDrop-isActiveDropTarget {
|
||||
&.domDroppable--hover {
|
||||
background-color: rgba(251, 208, 17, .25) !important;
|
||||
&:before {
|
||||
border-color: $euiColorVis5 !important;
|
||||
|
@ -84,61 +63,37 @@
|
|||
}
|
||||
}
|
||||
|
||||
.domDragDrop__container {
|
||||
position: relative;
|
||||
.domDroppable__container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&.domDragDrop__container-active {
|
||||
position: relative;
|
||||
&.domDroppable__container-active {
|
||||
z-index: $domDragDropZLevel3;
|
||||
}
|
||||
.domDroppable__container {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
$reorderItemMargin: $euiSizeS;
|
||||
.domDragDrop__reorderableDrop {
|
||||
.domDroppable--reorderable {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
height: calc(100% + #{calc($reorderItemMargin / 2)});
|
||||
}
|
||||
|
||||
.domDragDrop-translatableDrop {
|
||||
.domDroppable--translatable {
|
||||
transform: translateY(0);
|
||||
transition: transform $euiAnimSpeedFast ease-in-out;
|
||||
pointer-events: none;
|
||||
|
||||
.domDragDrop-isDropTarget {
|
||||
@include mixinDomDraggable;
|
||||
}
|
||||
|
||||
.domDragDrop-isActiveDropTarget {
|
||||
.domDroppable--hover {
|
||||
z-index: $domDragDropZLevel3;
|
||||
}
|
||||
}
|
||||
|
||||
.domDragDrop-translatableDrag {
|
||||
transform: translateY(0);
|
||||
transition: transform $euiAnimSpeedFast ease-in-out;
|
||||
position: relative;
|
||||
z-index: $domDragDropZLevel1;
|
||||
}
|
||||
|
||||
.domDragDrop__keyboardHandler {
|
||||
top: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: $euiBorderRadius;
|
||||
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
@include euiFocusRing;
|
||||
pointer-events: none;
|
||||
z-index: $domDragDropZLevel2;
|
||||
}
|
||||
}
|
||||
|
||||
.domDragDrop__extraDrops {
|
||||
.domDroppable__extraTargets {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
|
@ -150,58 +105,58 @@ $reorderItemMargin: $euiSizeS;
|
|||
max-width: $euiFormMaxWidth;
|
||||
}
|
||||
|
||||
.domDragDrop__extraDrops-visible {
|
||||
.domDroppable__extraTargets-visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.domDragDrop__extraDropWrapper {
|
||||
.domDroppable__extraDropWrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: $euiColorLightestShade;
|
||||
border-radius: $euiSizeXS;
|
||||
|
||||
.domDragDrop__extraDrop,
|
||||
.domDragDrop__extraDrop:before {
|
||||
.domDroppable__extraTarget,
|
||||
.domDroppable__extraTarget:before {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&:first-child .domDragDrop__extraDrop,
|
||||
&:first-child .domDragDrop__extraDrop:before {
|
||||
&:first-child .domDroppable__extraTarget,
|
||||
&:first-child .domDroppable__extraTarget:before {
|
||||
border-top-left-radius: $euiSizeXS;
|
||||
border-top-right-radius: $euiSizeXS;
|
||||
}
|
||||
|
||||
&:last-child .domDragDrop__extraDrop,
|
||||
&:last-child .domDragDrop__extraDrop:before {
|
||||
&:last-child .domDroppable__extraTarget,
|
||||
&:last-child .domDroppable__extraTarget:before {
|
||||
border-bottom-left-radius: $euiSizeXS;
|
||||
border-bottom-right-radius: $euiSizeXS;
|
||||
}
|
||||
}
|
||||
|
||||
// collapse borders
|
||||
.domDragDrop__extraDropWrapper + .domDragDrop__extraDropWrapper {
|
||||
.domDroppable__extraDropWrapper + .domDroppable__extraDropWrapper {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.domDragDrop__extraDrop {
|
||||
.domDroppable__extraTarget {
|
||||
position: relative;
|
||||
height: $euiSizeXS * 8;
|
||||
min-width: $euiSize * 7;
|
||||
color: $euiColorSuccessText;
|
||||
padding: $euiSizeXS;
|
||||
&.domDragDrop-incompatibleExtraDrop {
|
||||
&.domDroppable--incompatibleExtraTarget {
|
||||
color: $euiColorWarningText;
|
||||
}
|
||||
}
|
||||
|
||||
.domDragDrop__dropOverlayWrapper {
|
||||
.domDroppable__overlayWrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.domDragDrop__dropOverlay {
|
||||
.domDroppable_overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
@ -211,40 +166,21 @@ $reorderItemMargin: $euiSizeS;
|
|||
transition: $euiAnimSpeedFast ease-in-out;
|
||||
transition-property: background-color, border-color, opacity;
|
||||
|
||||
.domDragDrop-isDropTarget & {
|
||||
.domDroppable--active & {
|
||||
@include mixinDomDroppable($euiBorderWidthThick);
|
||||
@include mixinDomDroppableActive($euiBorderWidthThick);
|
||||
}
|
||||
|
||||
.domDragDrop-isActiveDropTarget & {
|
||||
@include mixinDomDroppableActiveHover($euiBorderWidthThick);
|
||||
.domDroppable--hover & {
|
||||
@include mixinDomDroppableHover($euiBorderWidthThick);
|
||||
}
|
||||
}
|
||||
|
||||
.domDragDrop-isActiveGroup {
|
||||
background-color: transparentize($euiColorVis0, .75);
|
||||
.domDragDrop-isKeyboardReorderInProgress {
|
||||
.domDragDrop--isDragStarted {
|
||||
.domDraggable_active_keyboard--reorderable {
|
||||
.domDraggable_dragover_keyboard--copy {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.domDragDrop-isActiveDropTarget,
|
||||
.domDragDrop-isDropTarget {
|
||||
background: $euiColorEmptyShade !important;
|
||||
}
|
||||
}
|
||||
|
||||
.domDragDrop--isDragStarted {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
// Draggable item when it is moving
|
||||
.domDragDrop-isHidden {
|
||||
opacity: 0;
|
||||
.domDragDrop__keyboardHandler {
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
55
packages/kbn-dom-drag-drop/src/test_utils.tsx
Normal file
55
packages/kbn-dom-drag-drop/src/test_utils.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { ReactElement } from 'react';
|
||||
import faker from 'faker';
|
||||
import { RenderOptions, render } from '@testing-library/react';
|
||||
import { DragContextState, RootDragDropProvider } from './providers';
|
||||
|
||||
export const EXACT = {
|
||||
exact: true,
|
||||
};
|
||||
|
||||
export const dataTransfer = {
|
||||
setData: jest.fn(),
|
||||
getData: jest.fn(),
|
||||
};
|
||||
|
||||
export const generateDragDropValue = (label = faker.lorem.word()) => ({
|
||||
id: faker.random.uuid(),
|
||||
humanData: {
|
||||
label,
|
||||
groupLabel: faker.lorem.word(),
|
||||
position: 1,
|
||||
canSwap: true,
|
||||
canDuplicate: true,
|
||||
layerNumber: 0,
|
||||
},
|
||||
});
|
||||
|
||||
export const renderWithDragDropContext = (
|
||||
ui: ReactElement,
|
||||
renderOptions?: RenderOptions,
|
||||
contextStateOverrides: Partial<DragContextState> = {}
|
||||
): any => {
|
||||
const { wrapper, ...options } = renderOptions || {};
|
||||
|
||||
const Wrapper: React.FC<{
|
||||
children: React.ReactNode;
|
||||
}> = ({ children }) => {
|
||||
return (
|
||||
<RootDragDropProvider initialState={contextStateOverrides}>{children}</RootDragDropProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const rtlRender = render(ui, { wrapper: Wrapper, ...options });
|
||||
|
||||
return {
|
||||
...rtlRender,
|
||||
};
|
||||
};
|
|
@ -42,7 +42,9 @@
|
|||
|
||||
.unifiedFieldListItemButton {
|
||||
width: 100%;
|
||||
|
||||
@include euiSlightShadow;
|
||||
background: $euiColorEmptyShade;
|
||||
border-radius: $euiBorderRadius;
|
||||
&.kbnFieldButton {
|
||||
&:focus-within,
|
||||
&-isActive {
|
||||
|
@ -66,6 +68,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.unifiedFieldList__fieldPopover__fieldPopoverPanel .unifiedFieldListItemButton {
|
||||
box-shadow: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.unifiedFieldListItemButton--missing {
|
||||
background: lightOrDarkTheme(transparentize($euiColorMediumShade, .9), $euiColorEmptyShade);
|
||||
color: $euiColorDarkShade;
|
||||
|
@ -96,7 +103,7 @@
|
|||
|
||||
&:hover,
|
||||
&[class*='-isActive'],
|
||||
.domDragDrop__keyboardHandler:focus + & {
|
||||
.domDraggable__keyboardHandler:focus + & {
|
||||
.unifiedFieldListItemButton__fieldIcon {
|
||||
opacity: 0;
|
||||
}
|
||||
|
|
|
@ -65,7 +65,9 @@ export const FieldList: React.FC<FieldListProps> = ({
|
|||
/>
|
||||
)}
|
||||
{!!prepend && <EuiFlexItem grow={false}>{prepend}</EuiFlexItem>}
|
||||
<EuiFlexItem grow={true}>{children}</EuiFlexItem>
|
||||
<EuiFlexItem className="unifiedFieldListSidebar__accordionContainer" grow={true}>
|
||||
{children}
|
||||
</EuiFlexItem>
|
||||
{!!append && <EuiFlexItem grow={false}>{append}</EuiFlexItem>}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
.unifiedFieldList__fieldsAccordion__titleTooltip {
|
||||
margin-right: $euiSizeXS;
|
||||
}
|
||||
|
||||
.unifiedFieldList__fieldsAccordion__fieldItems {
|
||||
// Quick fix for making sure the shadow and focus rings are visible outside the accordion bounds
|
||||
padding: $euiSizeXS;
|
||||
}
|
||||
|
|
|
@ -145,7 +145,7 @@ function InnerFieldsAccordion<T extends FieldListItem = DataViewField>({
|
|||
<EuiSpacer size="s" />
|
||||
{hasLoaded &&
|
||||
(!!fieldsCount ? (
|
||||
<ul className="unifiedFieldList__fieldsAccordion__fieldItems">
|
||||
<ul>
|
||||
{paginatedFields &&
|
||||
paginatedFields.map((field, index) => (
|
||||
<Fragment key={getFieldKey(field)}>
|
||||
|
|
|
@ -10,7 +10,7 @@ import React, { memo, useCallback, useMemo, useState } from 'react';
|
|||
import { EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
import { DragDrop } from '@kbn/dom-drag-drop';
|
||||
import { Draggable } from '@kbn/dom-drag-drop';
|
||||
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import type { SearchMode } from '../../types';
|
||||
import { FieldItemButton, type FieldItemButtonProps } from '../../components/field_item_button';
|
||||
|
@ -333,8 +333,8 @@ function UnifiedFieldListItemComponent({
|
|||
<FieldPopover
|
||||
isOpen={infoIsOpen}
|
||||
button={
|
||||
<DragDrop
|
||||
draggable
|
||||
<Draggable
|
||||
dragType="copy"
|
||||
dragClassName="unifiedFieldListItemButton__dragging"
|
||||
order={order}
|
||||
value={value}
|
||||
|
@ -361,7 +361,7 @@ function UnifiedFieldListItemComponent({
|
|||
size,
|
||||
})}
|
||||
/>
|
||||
</DragDrop>
|
||||
</Draggable>
|
||||
}
|
||||
closePopover={closePopover}
|
||||
data-test-subj={stateService.creationOptions.dataTestSubj?.fieldListItemPopoverDataTestSubj}
|
||||
|
|
|
@ -25,11 +25,29 @@
|
|||
}
|
||||
}
|
||||
|
||||
// This code is responsible for making sure that:
|
||||
// * the shadow and focus rings are visible outside the accordion bounds,
|
||||
// * the dragged element has a padding to the left and right,
|
||||
|
||||
.unifiedFieldListSidebar__list {
|
||||
padding: $euiSizeS $euiSizeS 0;
|
||||
padding: $euiSizeS $euiSizeXS 0 $euiSizeXS;
|
||||
|
||||
> *, .euiAccordion__triggerWrapper, .euiAccordion__children, .unifiedFieldListItemButton {
|
||||
padding-left: $euiSizeXS;
|
||||
padding-right: $euiSizeXS;
|
||||
}
|
||||
|
||||
.unifiedFieldListSidebar__accordionContainer {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
@include euiBreakpoint('xs', 's') {
|
||||
padding: $euiSizeS 0 0 0;
|
||||
> *, .euiAccordion__triggerWrapper, .unifiedFieldListSidebar__accordionContainer, .unifiedFieldListItemButton {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,14 +72,11 @@
|
|||
.unifiedFieldListSidebar .unifiedFieldListItemButton {
|
||||
&.kbnFieldButton {
|
||||
margin-bottom: calc($euiSizeXS / 2);
|
||||
}
|
||||
|
||||
&.domDragDrop-isDraggable {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:not(.unifiedFieldListItemButton__dragging) {
|
||||
padding: 0;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.unifiedFieldListItemButton__dragging {
|
||||
background: $euiColorEmptyShade;
|
||||
}
|
|
@ -14,6 +14,7 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
useEuiFontSize,
|
||||
useEuiShadow,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
|
@ -42,7 +43,7 @@ export interface DimensionButtonProps {
|
|||
message?: Message;
|
||||
}
|
||||
|
||||
export function DimensionButton({
|
||||
function DimensionButtonImpl({
|
||||
groupLabel,
|
||||
children,
|
||||
onClick,
|
||||
|
@ -56,6 +57,7 @@ export function DimensionButton({
|
|||
<div
|
||||
{...otherProps}
|
||||
css={css`
|
||||
${useEuiShadow('xs')}
|
||||
${useEuiFontSize('s')}
|
||||
border-radius: ${euiThemeVars.euiBorderRadius};
|
||||
position: relative;
|
||||
|
@ -66,6 +68,7 @@ export function DimensionButton({
|
|||
gap: ${euiThemeVars.euiSizeS};
|
||||
min-height: ${euiThemeVars.euiSizeXL};
|
||||
padding: ${euiThemeVars.euiSizeXS} ${euiThemeVars.euiSizeS};
|
||||
background: ${euiThemeVars.euiColorEmptyShade};
|
||||
`}
|
||||
>
|
||||
<EuiFlexGroup direction="row" alignItems="center" gutterSize="none" responsive={false}>
|
||||
|
@ -119,8 +122,8 @@ export function DimensionButton({
|
|||
transition-property: color, opacity, background-color, transform;
|
||||
opacity: 0;
|
||||
|
||||
.domDragDrop:hover &,
|
||||
.domDragDrop:focus-within & {
|
||||
.domDraggable:hover &,
|
||||
.domDraggable:focus-within & {
|
||||
opacity: 1;
|
||||
}
|
||||
&:hover,
|
||||
|
@ -133,3 +136,5 @@ export function DimensionButton({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const DimensionButton = React.memo(DimensionButtonImpl);
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
|
||||
import { DragDrop, type DropType, DropOverlayWrapper } from '@kbn/dom-drag-drop';
|
||||
import { type DropType, DropOverlayWrapper, Droppable } from '@kbn/dom-drag-drop';
|
||||
import React, { ReactElement, useCallback, useMemo } from 'react';
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
|
@ -106,8 +106,7 @@ export const DiscoverMainContent = ({
|
|||
const showChart = useAppStateSelector((state) => !state.hideChart);
|
||||
|
||||
return (
|
||||
<DragDrop
|
||||
draggable={false}
|
||||
<Droppable
|
||||
dropTypes={isDropAllowed ? DROP_PROPS.types : undefined}
|
||||
value={DROP_PROPS.value}
|
||||
order={DROP_PROPS.order}
|
||||
|
@ -144,6 +143,6 @@ export const DiscoverMainContent = ({
|
|||
)}
|
||||
</EuiFlexGroup>
|
||||
</DropOverlayWrapper>
|
||||
</DragDrop>
|
||||
</Droppable>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
|
||||
import { css } from '@emotion/react';
|
||||
import {
|
||||
DragDrop,
|
||||
Droppable,
|
||||
Draggable,
|
||||
DropTargetSwapDuplicateCombine,
|
||||
ReorderProvider,
|
||||
useDragDropContext,
|
||||
|
@ -93,10 +94,10 @@ export const AnnotationList = ({
|
|||
background-color: ${euiThemeVars.euiColorLightestShade};
|
||||
padding: ${euiThemeVars.euiSizeS};
|
||||
border-radius: ${euiThemeVars.euiBorderRadius};
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
|
||||
.domDragDrop-isActiveGroup {
|
||||
padding: ${euiThemeVars.euiSizeS};
|
||||
.domDragDrop-group {
|
||||
padding: ${euiThemeVars.euiSizeXS} ${euiThemeVars.euiSizeS};
|
||||
margin: -${euiThemeVars.euiSizeS} -${euiThemeVars.euiSizeS} 0 -${euiThemeVars.euiSizeS};
|
||||
}
|
||||
`}
|
||||
|
@ -106,11 +107,14 @@ export const AnnotationList = ({
|
|||
<div
|
||||
key={index}
|
||||
css={css`
|
||||
position: relative; // this is to properly contain the absolutely-positioned drop target in DragDrop
|
||||
margin-bottom: ${euiThemeVars.euiSizeS};
|
||||
position: relative; // this is to properly contain the absolutely-positioned drop target in Droppable
|
||||
& + div {
|
||||
margin-top: ${euiThemeVars.euiSizeS};
|
||||
}
|
||||
`}
|
||||
>
|
||||
<DragDrop
|
||||
<Draggable
|
||||
dragType="move"
|
||||
order={[index]}
|
||||
key={annotation.id}
|
||||
value={{
|
||||
|
@ -119,57 +123,72 @@ export const AnnotationList = ({
|
|||
label: annotation.label,
|
||||
},
|
||||
}}
|
||||
dragType="move"
|
||||
dropTypes={dragging && dragging.id !== annotation.id ? ['reorder'] : []}
|
||||
draggable
|
||||
reorderableGroup={annotations}
|
||||
onDrop={(source) => {
|
||||
const sourceAnnotation = source
|
||||
? annotations.find(({ id }) => id === source.id)
|
||||
: undefined;
|
||||
reorderAnnotations(sourceAnnotation, annotation);
|
||||
}}
|
||||
>
|
||||
<DimensionButton
|
||||
groupLabel={i18n.translate('eventAnnotationListing.groupEditor.addAnnotation', {
|
||||
defaultMessage: 'Annotations',
|
||||
})}
|
||||
onClick={() => selectAnnotation(annotation)}
|
||||
onRemoveClick={() =>
|
||||
updateAnnotations(annotations.filter(({ id }) => id !== annotation.id))
|
||||
}
|
||||
accessorConfig={getAnnotationAccessor(annotation)}
|
||||
label={annotation.label}
|
||||
<Droppable
|
||||
order={[index]}
|
||||
key={annotation.id}
|
||||
value={{
|
||||
id: annotation.id,
|
||||
humanData: {
|
||||
label: annotation.label,
|
||||
},
|
||||
}}
|
||||
dropTypes={dragging && dragging.id !== annotation.id ? ['reorder'] : []}
|
||||
reorderableGroup={annotations}
|
||||
onDrop={(source) => {
|
||||
const sourceAnnotation = source
|
||||
? annotations.find(({ id }) => id === source.id)
|
||||
: undefined;
|
||||
reorderAnnotations(sourceAnnotation, annotation);
|
||||
}}
|
||||
>
|
||||
<DimensionTrigger label={annotation.label} />
|
||||
</DimensionButton>
|
||||
</DragDrop>
|
||||
<DimensionButton
|
||||
groupLabel={i18n.translate('eventAnnotationListing.groupEditor.addAnnotation', {
|
||||
defaultMessage: 'Annotations',
|
||||
})}
|
||||
onClick={() => selectAnnotation(annotation)}
|
||||
onRemoveClick={() =>
|
||||
updateAnnotations(annotations.filter(({ id }) => id !== annotation.id))
|
||||
}
|
||||
accessorConfig={getAnnotationAccessor(annotation)}
|
||||
label={annotation.label}
|
||||
>
|
||||
<DimensionTrigger label={annotation.label} />
|
||||
</DimensionButton>
|
||||
</Droppable>
|
||||
</Draggable>
|
||||
</div>
|
||||
))}
|
||||
</ReorderProvider>
|
||||
|
||||
<DragDrop
|
||||
order={[annotations.length]}
|
||||
getCustomDropTarget={DropTargetSwapDuplicateCombine.getCustomDropTarget}
|
||||
getAdditionalClassesOnDroppable={
|
||||
DropTargetSwapDuplicateCombine.getAdditionalClassesOnDroppable
|
||||
}
|
||||
dropTypes={dragging ? ['duplicate_compatible'] : []}
|
||||
value={{
|
||||
id: 'addAnnotation',
|
||||
humanData: {
|
||||
label: addAnnotationText,
|
||||
},
|
||||
}}
|
||||
onDrop={({ id: sourceId }) => addNewAnnotation(sourceId)}
|
||||
<div
|
||||
css={css`
|
||||
margin-top: ${euiThemeVars.euiSizeXS};
|
||||
`}
|
||||
>
|
||||
<EmptyDimensionButton
|
||||
dataTestSubj="addAnnotation"
|
||||
label={addAnnotationText}
|
||||
ariaLabel={addAnnotationText}
|
||||
onClick={() => addNewAnnotation()}
|
||||
/>
|
||||
</DragDrop>
|
||||
<Droppable
|
||||
order={[annotations.length]}
|
||||
getCustomDropTarget={DropTargetSwapDuplicateCombine.getCustomDropTarget}
|
||||
getAdditionalClassesOnDroppable={
|
||||
DropTargetSwapDuplicateCombine.getAdditionalClassesOnDroppable
|
||||
}
|
||||
dropTypes={dragging ? ['duplicate_compatible'] : []}
|
||||
value={{
|
||||
id: 'addAnnotation',
|
||||
humanData: {
|
||||
label: addAnnotationText,
|
||||
},
|
||||
}}
|
||||
onDrop={({ id: sourceId }) => addNewAnnotation(sourceId)}
|
||||
>
|
||||
<EmptyDimensionButton
|
||||
dataTestSubj="addAnnotation"
|
||||
label={addAnnotationText}
|
||||
ariaLabel={addAnnotationText}
|
||||
onClick={() => addNewAnnotation()}
|
||||
/>
|
||||
</Droppable>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -807,12 +807,12 @@ export class DiscoverPageObject extends FtrService {
|
|||
* */
|
||||
public async dragFieldWithKeyboardToTable(fieldName: string) {
|
||||
const field = await this.find.byCssSelector(
|
||||
`[data-test-subj="domDragDrop_draggable-${fieldName}"] [data-test-subj="domDragDrop-keyboardHandler"]`
|
||||
`[data-test-subj="dscFieldListPanelField-${fieldName}"] [data-test-subj="domDragDrop-keyboardHandler"]`
|
||||
);
|
||||
await field.focus();
|
||||
await this.retry.try(async () => {
|
||||
await this.browser.pressKeys(this.browser.keys.ENTER);
|
||||
await this.testSubjects.exists('.domDragDrop-isDropTarget'); // checks if we're in dnd mode and there's any drop target active
|
||||
await this.testSubjects.exists('.domDroppable--active'); // checks if we're in dnd mode and there's any drop target active
|
||||
});
|
||||
await this.browser.pressKeys(this.browser.keys.RIGHT);
|
||||
await this.browser.pressKeys(this.browser.keys.ENTER);
|
||||
|
|
|
@ -60,4 +60,4 @@
|
|||
|
||||
.mappingsEditor__fieldsListItem__actions {
|
||||
padding-left: $euiSizeS;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
.lnsFieldItem__fieldPanel {
|
||||
min-width: 260px;
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
|
@ -23,7 +23,6 @@ import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
|||
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
|
||||
import { DataView, DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { loadFieldStats } from '@kbn/unified-field-list/src/services/field_stats';
|
||||
import { DOCUMENT_FIELD_NAME } from '../../../common/constants';
|
||||
import { FieldIcon } from '@kbn/field-utils';
|
||||
import { FieldStats, FieldPopoverFooter } from '@kbn/unified-field-list';
|
||||
|
||||
|
@ -31,7 +30,7 @@ jest.mock('@kbn/unified-field-list/src/services/field_stats', () => ({
|
|||
loadFieldStats: jest.fn().mockResolvedValue({}),
|
||||
}));
|
||||
|
||||
const clickField = async (wrapper: ReactWrapper, field: string) => {
|
||||
const clickField = async (wrapper: ReactWrapper, field?: string) => {
|
||||
await act(async () => {
|
||||
await wrapper
|
||||
.find(`[data-test-subj="lnsFieldListPanelField-${field}"] .kbnFieldButton__button`)
|
||||
|
@ -91,14 +90,14 @@ describe('Lens Field Item', () => {
|
|||
fields: [
|
||||
{
|
||||
name: 'timestamp',
|
||||
displayName: 'timestampLabel',
|
||||
displayName: 'timestamp',
|
||||
type: 'date',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'bytes',
|
||||
displayName: 'bytesLabel',
|
||||
displayName: 'bytes',
|
||||
type: 'number',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
|
@ -154,7 +153,7 @@ describe('Lens Field Item', () => {
|
|||
filters: [],
|
||||
field: {
|
||||
name: 'bytes',
|
||||
displayName: 'bytesLabel',
|
||||
displayName: 'bytes',
|
||||
type: 'number',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
|
@ -189,7 +188,7 @@ describe('Lens Field Item', () => {
|
|||
// Using .toContain over .toEqual because this element includes text from <EuiScreenReaderOnly>
|
||||
// which can't be seen, but shows in the text content
|
||||
expect(wrapper.find('[data-test-subj="lnsFieldListPanelField"]').first().text()).toContain(
|
||||
'bytesLabel'
|
||||
'bytes'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -260,7 +259,7 @@ describe('Lens Field Item', () => {
|
|||
|
||||
const field = {
|
||||
name: 'test',
|
||||
displayName: 'testLabel',
|
||||
displayName: 'test',
|
||||
type: 'string',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
|
@ -422,7 +421,7 @@ describe('Lens Field Item', () => {
|
|||
field: documentField,
|
||||
});
|
||||
|
||||
await clickField(wrapper, DOCUMENT_FIELD_NAME);
|
||||
await clickField(wrapper, documentField.name);
|
||||
|
||||
await wrapper.update();
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
FieldItemButton,
|
||||
type GetCustomFieldType,
|
||||
} from '@kbn/unified-field-list';
|
||||
import { DragDrop } from '@kbn/dom-drag-drop';
|
||||
import { Draggable } from '@kbn/dom-drag-drop';
|
||||
import { generateFilters, getEsQueryConfig } from '@kbn/data-plugin/public';
|
||||
import { type DatatableColumn } from '@kbn/expressions-plugin/common';
|
||||
import { DatasourceDataPanelProps } from '../../types';
|
||||
|
@ -200,19 +200,18 @@ export function InnerFieldItem(props: FieldItemProps) {
|
|||
closePopover={closePopover}
|
||||
panelClassName="lnsFieldItem__fieldPanel"
|
||||
initialFocus=".lnsFieldItem__fieldPanel"
|
||||
className="lnsFieldItem__popoverAnchor"
|
||||
data-test-subj="lnsFieldListPanelField"
|
||||
panelProps={{
|
||||
'data-test-subj': 'lnsFieldListPanelFieldContent',
|
||||
}}
|
||||
container={document.querySelector<HTMLElement>('.application') || undefined}
|
||||
button={
|
||||
<DragDrop
|
||||
draggable
|
||||
order={order}
|
||||
<Draggable
|
||||
dragType="copy"
|
||||
value={value}
|
||||
dataTestSubj={`lnsFieldListPanelField-${field.name}`}
|
||||
order={order}
|
||||
onDragStart={closePopover}
|
||||
dataTestSubj={`lnsFieldListPanelField-${field.name}`}
|
||||
>
|
||||
{isTextBasedColumnField(field) ? (
|
||||
<FieldItemButton<DatatableColumn>
|
||||
|
@ -223,7 +222,7 @@ export function InnerFieldItem(props: FieldItemProps) {
|
|||
) : (
|
||||
<FieldItemButton field={field} {...commonFieldItemButtonProps} />
|
||||
)}
|
||||
</DragDrop>
|
||||
</Draggable>
|
||||
}
|
||||
renderHeader={() => {
|
||||
return (
|
||||
|
|
|
@ -7,11 +7,12 @@
|
|||
|
||||
import React, { useMemo, useCallback, ReactElement } from 'react';
|
||||
import {
|
||||
DragDrop,
|
||||
DragDropIdentifier,
|
||||
useDragDropContext,
|
||||
DropType,
|
||||
DropTargetSwapDuplicateCombine,
|
||||
Draggable,
|
||||
Droppable,
|
||||
} from '@kbn/dom-drag-drop';
|
||||
import { isDraggedField } from '../../../../utils';
|
||||
import {
|
||||
|
@ -50,8 +51,8 @@ export function DraggableDimensionButton({
|
|||
};
|
||||
order: [2, number, number, number];
|
||||
onDrop: (source: DragDropIdentifier, dropTarget: DragDropIdentifier, dropType?: DropType) => void;
|
||||
onDragStart: () => void;
|
||||
onDragEnd: () => void;
|
||||
onDragStart?: () => void;
|
||||
onDragEnd?: () => void;
|
||||
activeVisualization: Visualization<unknown, unknown>;
|
||||
group: VisualizationDimensionGroupConfig;
|
||||
children: ReactElement;
|
||||
|
@ -138,24 +139,29 @@ export function DraggableDimensionButton({
|
|||
className="lnsLayerPanel__dimensionContainer"
|
||||
data-test-subj={group.dataTestSubj}
|
||||
>
|
||||
<DragDrop
|
||||
draggable
|
||||
getCustomDropTarget={DropTargetSwapDuplicateCombine.getCustomDropTarget}
|
||||
getAdditionalClassesOnEnter={DropTargetSwapDuplicateCombine.getAdditionalClassesOnEnter}
|
||||
getAdditionalClassesOnDroppable={
|
||||
DropTargetSwapDuplicateCombine.getAdditionalClassesOnDroppable
|
||||
}
|
||||
order={order}
|
||||
<Draggable
|
||||
dragType={isOperation(dragging) ? 'move' : 'copy'}
|
||||
dropTypes={dropTypes}
|
||||
order={order}
|
||||
reorderableGroup={reorderableGroup.length > 1 ? reorderableGroup : undefined}
|
||||
value={value}
|
||||
onDrop={handleOnDrop}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
{children}
|
||||
</DragDrop>
|
||||
<Droppable
|
||||
dropTypes={dropTypes}
|
||||
getCustomDropTarget={DropTargetSwapDuplicateCombine.getCustomDropTarget}
|
||||
getAdditionalClassesOnEnter={DropTargetSwapDuplicateCombine.getAdditionalClassesOnEnter}
|
||||
getAdditionalClassesOnDroppable={
|
||||
DropTargetSwapDuplicateCombine.getAdditionalClassesOnDroppable
|
||||
}
|
||||
order={order}
|
||||
reorderableGroup={reorderableGroup.length > 1 ? reorderableGroup : undefined}
|
||||
value={value}
|
||||
onDrop={handleOnDrop}
|
||||
>
|
||||
{children}
|
||||
</Droppable>
|
||||
</Draggable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,11 +9,11 @@ import React, { useMemo, useState, useEffect } from 'react';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
DragDrop,
|
||||
DragDropIdentifier,
|
||||
useDragDropContext,
|
||||
DropType,
|
||||
DropTargetSwapDuplicateCombine,
|
||||
Droppable,
|
||||
} from '@kbn/dom-drag-drop';
|
||||
import { EmptyDimensionButton as EmptyDimensionButtonInner } from '@kbn/visualization-ui-components';
|
||||
import { css } from '@emotion/react';
|
||||
|
@ -180,7 +180,7 @@ export function EmptyDimensionButton({
|
|||
|
||||
return (
|
||||
<div className="lnsLayerPanel__dimensionContainer" data-test-subj={group.dataTestSubj}>
|
||||
<DragDrop
|
||||
<Droppable
|
||||
getCustomDropTarget={DropTargetSwapDuplicateCombine.getCustomDropTarget}
|
||||
getAdditionalClassesOnDroppable={
|
||||
DropTargetSwapDuplicateCombine.getAdditionalClassesOnDroppable
|
||||
|
@ -201,7 +201,7 @@ export function EmptyDimensionButton({
|
|||
<DefaultEmptyButton {...buttonProps} />
|
||||
)}
|
||||
</div>
|
||||
</DragDrop>
|
||||
</Droppable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.domDragDrop-isReplacing {
|
||||
.domDroppable--replacing {
|
||||
.dimensionTrigger__textLabel {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React from 'react';
|
||||
import { screen, fireEvent, act } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ChildDragDropProvider, DragDrop } from '@kbn/dom-drag-drop';
|
||||
import { ChildDragDropProvider, Droppable, Draggable } from '@kbn/dom-drag-drop';
|
||||
import { FramePublicAPI, Visualization, VisualizationConfigProps } from '../../../types';
|
||||
import { LayerPanel } from './layer_panel';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
|
@ -25,7 +25,7 @@ import {
|
|||
import { createIndexPatternServiceMock } from '../../../mocks/data_views_service_mock';
|
||||
import { DimensionButton } from '@kbn/visualization-ui-components';
|
||||
import { LensAppState } from '../../../state_management';
|
||||
import { ProviderProps } from '@kbn/dom-drag-drop/src';
|
||||
import type { ProviderProps } from '@kbn/dom-drag-drop/src';
|
||||
import { LayerPanelProps } from './types';
|
||||
|
||||
jest.mock('../../../id_generator');
|
||||
|
@ -676,9 +676,9 @@ describe('LayerPanel', () => {
|
|||
source: draggingField,
|
||||
})
|
||||
);
|
||||
const dragDropElement = screen.getByTestId('lnsDragDrop');
|
||||
fireEvent.dragOver(dragDropElement);
|
||||
fireEvent.drop(dragDropElement);
|
||||
const droppableElement = screen.getByTestId('lnsDragDrop-domDroppable');
|
||||
fireEvent.dragOver(droppableElement);
|
||||
fireEvent.drop(droppableElement);
|
||||
|
||||
expect(onDropToDimension).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -720,17 +720,17 @@ describe('LayerPanel', () => {
|
|||
);
|
||||
|
||||
expect(
|
||||
instance.find('[data-test-subj="lnsGroupTestId"] DragDrop').first().prop('dropType')
|
||||
instance.find('[data-test-subj="lnsGroupTestId"] Droppable').first().prop('dropType')
|
||||
).toEqual(undefined);
|
||||
|
||||
const dragDropElement = instance
|
||||
.find('[data-test-subj="lnsGroupTestId"] DragDrop')
|
||||
const droppableElement = instance
|
||||
.find('[data-test-subj="lnsGroupTestId"] Droppable')
|
||||
.first()
|
||||
.find(DimensionButton)
|
||||
.first();
|
||||
|
||||
dragDropElement.simulate('dragOver');
|
||||
dragDropElement.simulate('drop');
|
||||
droppableElement.simulate('dragOver');
|
||||
droppableElement.simulate('drop');
|
||||
|
||||
expect(onDropToDimension).not.toHaveBeenCalled();
|
||||
});
|
||||
|
@ -774,11 +774,11 @@ describe('LayerPanel', () => {
|
|||
|
||||
// Simulate drop on the pre-populated dimension
|
||||
|
||||
const dragDropElement = instance
|
||||
.find('[data-test-subj="lnsGroupTestId2"] DragDrop .domDragDrop')
|
||||
const droppableElement = instance
|
||||
.find('[data-test-subj="lnsGroupTestId2"] Droppable .domDroppable')
|
||||
.at(0);
|
||||
dragDropElement.simulate('dragOver');
|
||||
dragDropElement.simulate('drop');
|
||||
droppableElement.simulate('dragOver');
|
||||
droppableElement.simulate('drop');
|
||||
|
||||
expect(onDropToDimension).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -791,12 +791,12 @@ describe('LayerPanel', () => {
|
|||
|
||||
// Simulate drop on the empty dimension
|
||||
|
||||
const updatedDragDropElement = instance
|
||||
.find('[data-test-subj="lnsGroupTestId2"] DragDrop .domDragDrop')
|
||||
const updatedDroppableElement = instance
|
||||
.find('[data-test-subj="lnsGroupTestId2"] Droppable .domDroppable')
|
||||
.last();
|
||||
|
||||
updatedDragDropElement.simulate('dragOver');
|
||||
updatedDragDropElement.simulate('drop');
|
||||
updatedDroppableElement.simulate('dragOver');
|
||||
updatedDroppableElement.simulate('drop');
|
||||
|
||||
expect(onDropToDimension).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -828,7 +828,7 @@ describe('LayerPanel', () => {
|
|||
{ attachTo: holder }
|
||||
);
|
||||
act(() => {
|
||||
instance.find(DragDrop).at(1).prop('onDrop')!(draggingOperation, 'reorder');
|
||||
instance.find(Droppable).at(1).prop('onDrop')!(draggingOperation, 'reorder');
|
||||
});
|
||||
expect(onDropToDimension).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -837,7 +837,7 @@ describe('LayerPanel', () => {
|
|||
})
|
||||
);
|
||||
const secondButton = instance
|
||||
.find(DragDrop)
|
||||
.find(Draggable)
|
||||
.at(1)
|
||||
.find('[data-test-subj="lnsDragDrop-keyboardHandler"]')
|
||||
.at(1)
|
||||
|
@ -864,7 +864,7 @@ describe('LayerPanel', () => {
|
|||
</ChildDragDropProvider>
|
||||
);
|
||||
act(() => {
|
||||
instance.find(DragDrop).at(2).prop('onDrop')!(draggingOperation, 'duplicate_compatible');
|
||||
instance.find(Droppable).at(2).prop('onDrop')!(draggingOperation, 'duplicate_compatible');
|
||||
});
|
||||
expect(onDropToDimension).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -901,7 +901,7 @@ describe('LayerPanel', () => {
|
|||
</ChildDragDropProvider>
|
||||
);
|
||||
act(() => {
|
||||
instance.find(DragDrop).at(3).prop('onDrop')!(draggingOperation, 'replace_compatible');
|
||||
instance.find(Droppable).at(3).prop('onDrop')!(draggingOperation, 'replace_compatible');
|
||||
});
|
||||
expect(onDropToDimension).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -946,7 +946,7 @@ describe('LayerPanel', () => {
|
|||
</ChildDragDropProvider>
|
||||
);
|
||||
act(() => {
|
||||
instance.find(DragDrop).at(3).prop('onDrop')!(draggingOperation, 'replace_compatible');
|
||||
instance.find(Droppable).at(3).prop('onDrop')!(draggingOperation, 'replace_compatible');
|
||||
});
|
||||
|
||||
expect(onDropToDimension).toHaveBeenCalledWith(
|
||||
|
@ -997,7 +997,7 @@ describe('LayerPanel', () => {
|
|||
</ChildDragDropProvider>
|
||||
);
|
||||
act(() => {
|
||||
instance.find(DragDrop).at(3).prop('onDrop')!(draggingOperation, 'replace_compatible');
|
||||
instance.find(Droppable).at(3).prop('onDrop')!(draggingOperation, 'replace_compatible');
|
||||
});
|
||||
|
||||
expect(onDropToDimension).toHaveBeenCalledWith(
|
||||
|
@ -1033,7 +1033,7 @@ describe('LayerPanel', () => {
|
|||
</ChildDragDropProvider>
|
||||
);
|
||||
act(() => {
|
||||
instance.find(DragDrop).at(3).prop('onDrop')!(draggingOperation, 'duplicate_compatible');
|
||||
instance.find(Droppable).at(3).prop('onDrop')!(draggingOperation, 'duplicate_compatible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -49,8 +49,6 @@ export function LayerPanel(props: LayerPanelProps) {
|
|||
|
||||
const [isPanelSettingsOpen, setPanelSettingsOpen] = useState(false);
|
||||
|
||||
const [hideTooltip, setHideTooltip] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
framePublicAPI,
|
||||
layerId,
|
||||
|
@ -552,8 +550,6 @@ export function LayerPanel(props: LayerPanelProps) {
|
|||
state={layerDatasourceState}
|
||||
layerDatasource={layerDatasource}
|
||||
datasourceLayers={framePublicAPI.datasourceLayers}
|
||||
onDragStart={() => setHideTooltip(true)}
|
||||
onDragEnd={() => setHideTooltip(false)}
|
||||
onDrop={onDrop}
|
||||
indexPatterns={dataViews.indexPatterns}
|
||||
>
|
||||
|
@ -591,7 +587,6 @@ export function LayerPanel(props: LayerPanelProps) {
|
|||
{activeVisualization?.DimensionTriggerComponent?.({
|
||||
columnId,
|
||||
label: columnLabelMap?.[columnId] ?? '',
|
||||
hideTooltip,
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -29,7 +29,7 @@ import {
|
|||
renderWithReduxStore,
|
||||
} from '../../mocks';
|
||||
import { inspectorPluginMock } from '@kbn/inspector-plugin/public/mocks';
|
||||
import { DragDrop, useDragDropContext } from '@kbn/dom-drag-drop';
|
||||
import { Droppable, useDragDropContext } from '@kbn/dom-drag-drop';
|
||||
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
|
||||
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
|
||||
import { expressionsPluginMock } from '@kbn/expressions-plugin/public/mocks';
|
||||
|
@ -626,7 +626,7 @@ describe('editor_frame', () => {
|
|||
instance.update();
|
||||
|
||||
act(() => {
|
||||
instance.find('[data-test-subj="mockVisA"]').find(DragDrop).prop('onDrop')!(
|
||||
instance.find('[data-test-subj="mockVisA"]').find(Droppable).prop('onDrop')!(
|
||||
{
|
||||
indexPatternId: '1',
|
||||
field: {},
|
||||
|
@ -711,7 +711,7 @@ describe('editor_frame', () => {
|
|||
instance.update();
|
||||
|
||||
act(() => {
|
||||
instance.find(DragDrop).filter('[dataTestSubj="lnsWorkspace"]').prop('onDrop')!(
|
||||
instance.find(Droppable).filter('[dataTestSubj="lnsWorkspace"]').prop('onDrop')!(
|
||||
{
|
||||
indexPatternId: '1',
|
||||
field: {},
|
||||
|
|
|
@ -115,7 +115,10 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<RootDragDropProvider dataTestSubj="lnsDragDrop" customMiddleware={telemetryMiddleware}>
|
||||
<RootDragDropProvider
|
||||
initialState={{ dataTestSubjPrefix: 'lnsDragDrop' }}
|
||||
customMiddleware={telemetryMiddleware}
|
||||
>
|
||||
<FrameLayout
|
||||
bannerMessages={
|
||||
bannerMessages.length ? (
|
||||
|
|
|
@ -11,7 +11,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { UiActionsStart, VISUALIZE_GEO_FIELD_TRIGGER } from '@kbn/ui-actions-plugin/public';
|
||||
import { GlobeIllustration } from '@kbn/chart-icons';
|
||||
import { DragDrop } from '@kbn/dom-drag-drop';
|
||||
import { Droppable } from '@kbn/dom-drag-drop';
|
||||
import { IndexPattern } from '../../../types';
|
||||
import { getVisualizeGeoFieldMessage } from '../../../utils';
|
||||
import { APP_ID } from '../../../../common/constants';
|
||||
|
@ -52,10 +52,9 @@ export function GeoFieldWorkspacePanel(props: Props) {
|
|||
<strong>{getVisualizeGeoFieldMessage(props.fieldType)}</strong>
|
||||
</h2>
|
||||
<GlobeIllustration aria-hidden={true} className="lnsWorkspacePanel__promptIllustration" />
|
||||
<DragDrop
|
||||
<Droppable
|
||||
className="lnsVisualizeGeoFieldWorkspacePanel__dragDrop"
|
||||
dataTestSubj="lnsGeoFieldWorkspace"
|
||||
draggable={false}
|
||||
dropTypes={['field_add']}
|
||||
order={dragDropOrder}
|
||||
value={dragDropIdentifier}
|
||||
|
@ -69,7 +68,7 @@ export function GeoFieldWorkspacePanel(props: Props) {
|
|||
/>
|
||||
</strong>
|
||||
</p>
|
||||
</DragDrop>
|
||||
</Droppable>
|
||||
</div>
|
||||
</EuiText>
|
||||
</div>
|
||||
|
|
|
@ -936,14 +936,12 @@ describe('workspace_panel', () => {
|
|||
datasourceState: {},
|
||||
}),
|
||||
});
|
||||
expect(screen.getByTestId('lnsWorkspace').classList).toContain('domDragDrop-isDropTarget');
|
||||
expect(screen.getByTestId('lnsWorkspace').classList).toContain('domDroppable--active');
|
||||
});
|
||||
|
||||
it('should refuse to drop if there are no suggestions', () => {
|
||||
renderWithDndAndRedux();
|
||||
expect(screen.getByTestId('lnsWorkspace').classList).not.toContain(
|
||||
'domDragDrop-isDropTarget'
|
||||
);
|
||||
expect(screen.getByTestId('lnsWorkspace').classList).not.toContain('domDroppable--active');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,7 +25,7 @@ import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public';
|
|||
import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common';
|
||||
import type { Datatable } from '@kbn/expressions-plugin/public';
|
||||
import { DropIllustration } from '@kbn/chart-icons';
|
||||
import { DragDrop, useDragDropContext, DragDropIdentifier } from '@kbn/dom-drag-drop';
|
||||
import { useDragDropContext, DragDropIdentifier, Droppable } from '@kbn/dom-drag-drop';
|
||||
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
|
||||
import { ChartSizeSpec, isChartSizeEvent } from '@kbn/chart-expressions-common';
|
||||
import { trackUiCounterEvents } from '../../../lens_ui_telemetry';
|
||||
|
@ -638,19 +638,18 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
: renderDragDropPrompt;
|
||||
|
||||
return (
|
||||
<DragDrop
|
||||
<Droppable
|
||||
className={classNames('lnsWorkspacePanel__dragDrop', {
|
||||
'lnsWorkspacePanel__dragDrop--fullscreen': isFullscreen,
|
||||
})}
|
||||
dataTestSubj="lnsWorkspace"
|
||||
draggable={false}
|
||||
dropTypes={suggestionForDraggedField ? ['field_add'] : undefined}
|
||||
onDrop={onDrop}
|
||||
value={dropProps.value}
|
||||
order={dropProps.order}
|
||||
>
|
||||
<div className="lnsWorkspacePanelWrapper__pageContentBody">{renderWorkspaceContents()}</div>
|
||||
</DragDrop>
|
||||
</Droppable>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
}
|
||||
|
||||
.lnsWorkspacePanel__dragDrop {
|
||||
&.domDragDrop-isDropTarget {
|
||||
&.domDroppable--active {
|
||||
p {
|
||||
transition: filter $euiAnimSpeedFast ease-in-out;
|
||||
filter: blur(5px);
|
||||
|
@ -59,7 +59,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.domDragDrop-isActiveDropTarget {
|
||||
&.domDroppable--hover {
|
||||
.lnsDropIllustration__hand {
|
||||
animation: lnsWorkspacePanel__illustrationPulseContinuous 1.5s ease-in-out 0s infinite normal forwards;
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ export function createMockedDragDropContext(
|
|||
dataTestSubjPrefix: 'lnsDragDrop',
|
||||
dragging: undefined,
|
||||
keyboardMode: false,
|
||||
activeDropTarget: undefined,
|
||||
hoveredDropTarget: undefined,
|
||||
dropTargetsByOrder: undefined,
|
||||
...partialState,
|
||||
},
|
||||
|
|
|
@ -1241,8 +1241,7 @@ export interface Visualization<T = unknown, P = T, ExtraAppendLayerArg = unknown
|
|||
DimensionTriggerComponent?: (props: {
|
||||
columnId: string;
|
||||
label: string;
|
||||
hideTooltip?: boolean;
|
||||
}) => null | ReactElement<{ columnId: string; label: string; hideTooltip?: boolean }>;
|
||||
}) => null | ReactElement<{ columnId: string; label: string }>;
|
||||
getAddLayerButtonComponent?: (
|
||||
props: AddLayerButtonProps
|
||||
) => null | ReactElement<AddLayerButtonProps>;
|
||||
|
|
|
@ -116,7 +116,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await retry.try(async () => {
|
||||
const breakdownLabel = await testSubjects.find(
|
||||
'lnsDragDrop_draggable-Top 3 values of extension.raw'
|
||||
'lnsDragDrop_domDraggable_Top 3 values of extension.raw'
|
||||
);
|
||||
|
||||
const lnsWorkspace = await testSubjects.find('lnsWorkspace');
|
||||
|
|
|
@ -370,7 +370,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
await retry.try(async () => {
|
||||
await browser.pressKeys(browserKey);
|
||||
await find.existsByCssSelector(
|
||||
`.domDragDrop__extraDrop > [data-test-subj="domDragDrop-dropTarget-${metaToAction[metaKey]}"].domDragDrop-isActiveDropTarget`
|
||||
`.domDroppable__extraTarget > [data-test-subj="domDragDrop-dropTarget-${metaToAction[metaKey]}"].domDroppable--hover`
|
||||
);
|
||||
});
|
||||
},
|
||||
|
@ -390,12 +390,12 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
metaKey?: 'shift' | 'alt' | 'ctrl'
|
||||
) {
|
||||
const field = await find.byCssSelector(
|
||||
`[data-test-subj="lnsDragDrop_draggable-${fieldName}"] [data-test-subj="lnsDragDrop-keyboardHandler"]`
|
||||
`[data-test-subj="lnsFieldListPanelField-${fieldName}"] [data-test-subj="lnsDragDrop-keyboardHandler"]`
|
||||
);
|
||||
await field.focus();
|
||||
await retry.try(async () => {
|
||||
await browser.pressKeys(browser.keys.ENTER);
|
||||
await testSubjects.exists('.domDragDrop-isDropTarget'); // checks if we're in dnd mode and there's any drop target active
|
||||
await testSubjects.exists('.domDroppable--active'); // checks if we're in dnd mode and there's any drop target active
|
||||
});
|
||||
for (let i = 0; i < steps; i++) {
|
||||
await browser.pressKeys(reverse ? browser.keys.LEFT : browser.keys.RIGHT);
|
||||
|
@ -515,7 +515,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
* @param endIndex - the index of drop starting from 1
|
||||
* */
|
||||
async reorderDimensions(dimension: string, startIndex: number, endIndex: number) {
|
||||
const dragging = `[data-test-subj='${dimension}']:nth-of-type(${startIndex}) .domDragDrop`;
|
||||
const dragging = `[data-test-subj='${dimension}']:nth-of-type(${startIndex}) .domDraggable`;
|
||||
const dropping = `[data-test-subj='${dimension}']:nth-of-type(${endIndex}) [data-test-subj='lnsDragDrop-reorderableDropLayer'`;
|
||||
await find.existsByCssSelector(dragging);
|
||||
await browser.html5DragAndDrop(dragging, dropping);
|
||||
|
@ -1611,7 +1611,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
const from = `lnsFieldListPanelField-${field}`;
|
||||
await this.dragEnterDrop(
|
||||
testSubjects.getCssSelector(from),
|
||||
testSubjects.getCssSelector(`${to} > lnsDragDrop`),
|
||||
testSubjects.getCssSelector(`${to} > lnsDragDrop-domDroppable`),
|
||||
testSubjects.getCssSelector(`${to} > domDragDrop-dropTarget-${type}`)
|
||||
);
|
||||
await this.waitForVisualization(visDataTestSubj);
|
||||
|
@ -1632,7 +1632,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
) {
|
||||
await this.dragEnterDrop(
|
||||
testSubjects.getCssSelector(from),
|
||||
testSubjects.getCssSelector(`${to} > lnsDragDrop`),
|
||||
testSubjects.getCssSelector(`${to} > lnsDragDrop-domDroppable`),
|
||||
testSubjects.getCssSelector(`${to} > domDragDrop-dropTarget-${type}`)
|
||||
);
|
||||
await this.waitForVisualization(visDataTestSubj);
|
||||
|
@ -1808,7 +1808,12 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
`[data-test-subj="lnsIndexPattern${groupCapitalized}Fields"] .unifiedFieldListItemButton--${type}`
|
||||
);
|
||||
// map to testSubjId
|
||||
return Promise.all(allFieldsForType.map((el) => el.getAttribute('data-test-subj')));
|
||||
return Promise.all(
|
||||
allFieldsForType.map(async (el) => {
|
||||
const parent = await el.findByXpath('./..');
|
||||
return parent.getAttribute('data-test-subj');
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
async clickShareMenu() {
|
||||
|
|
|
@ -104,7 +104,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await retry.try(async () => {
|
||||
const breakdownLabel = await testSubjects.find(
|
||||
'lnsDragDrop_draggable-Top 3 values of extension.raw'
|
||||
'lnsDragDrop_domDraggable_Top 3 values of extension.raw'
|
||||
);
|
||||
|
||||
const lnsWorkspace = await testSubjects.find('lnsWorkspace');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue