[Lens] Refactor drag and drop (#161257)

## Summary

When I created drag and drop for Lens, the API I went for was not the
most readable one. It was designed this way because I wanted to gain
some performance, but it was very hard to maintain the performance gain
with a lot of changes in the drag and drop area because all the pieces
of the code needed to memoized in a tricky way and it wasn't
communicated well.
In the end it works even without these tricks so I decided to simplify
it in this PR.

The main changes include:

1. Instead of multiple `useState` per parameter, we keep all the state
in reducer both for `ReorderProvider` and `RootDragDropProvider`. Thanks
to that we get multiple improvements:
2. The code in `DragDrop` component becomes more descriptive as we don't
run multiple state updates when user executes an action but one state
update describing what actually happens (eg. `dispatchDnd({type:
'selectDropTarget' ....})`. The internal logic of the update lives in
the reducer.
3. We don't have to pass `trackUiCounterEvents` as another prop to
`DragDrop` and run it wherever we need - instead we pass it as a
middleware to the context and run before dispatching (and it's very easy
to add more middlewares if we need extra integrations at some point!)
4. We also run a11y announcements as a middleware instead of inside
`DragDrop` component
5. The `ChildDragDropProvider` props look much cleaner:
    before:
```
      <ChildDragDropProvider
        keyboardMode={keyboardModeState}
        setKeyboardMode={setKeyboardModeState}
        dragging={draggingState.dragging}
        setA11yMessage={setA11yMessage}
        setDragging={setDragging}
        activeDropTarget={activeDropTargetState}
        setActiveDropTarget={setActiveDropTarget}
        registerDropTarget={registerDropTarget}
        dropTargetsByOrder={dropTargetsByOrderState}
        dataTestSubjPrefix={dataTestSubj}
        onTrackUICounterEvent={onTrackUICounterEvent}
      >
        {children}
      </ChildDragDropProvider>
```
after:
```
<ChildDragDropProvider value={[state, dispatch]}>{children}</ChildDragDropProvider>
```
6. Created custom hook `useDragDropContext` instead of using
`useContext(DragContext)` and making DragContext private. This way we
will avoid potential problems with using context outside of root.
7. Bonus thing - if we ever decide to move to redux, the structure is
there already



What I am still not happy with is that the tests are very
domain-dependant instead of user-driven - instead of checking the store
actions, I should check the interface from the user perspective. I will
try to work on it once I find some time between more important tasks
though.
This commit is contained in:
Marta Bondyra 2023-07-11 13:05:03 +02:00 committed by GitHub
parent 203c9b04b6
commit 91a0d2f454
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1208 additions and 1109 deletions

View file

@ -14,8 +14,8 @@
* Side Public License, v 1.
*/
import React, { useContext, useMemo } from 'react';
import { DragContext, DragDrop, DropOverlayWrapper, DropType } from '@kbn/dom-drag-drop';
import React, { useMemo } from 'react';
import { DragDrop, DropOverlayWrapper, DropType, useDragDropContext } from '@kbn/dom-drag-drop';
import { EuiEmptyPrompt, EuiPanel } from '@elastic/eui';
const DROP_PROPS = {
@ -34,8 +34,8 @@ export interface ExampleDropZoneProps {
}
export const ExampleDropZone: React.FC<ExampleDropZoneProps> = ({ onDropField }) => {
const dragDropContext = useContext(DragContext);
const draggingFieldName = dragDropContext.dragging?.id;
const [{ dragging }] = useDragDropContext();
const draggingFieldName = dragging?.id;
const onDroppingField = useMemo(() => {
if (!draggingFieldName) {

View file

@ -14,10 +14,10 @@
* Side Public License, v 1.
*/
import React, { useCallback, useContext, useMemo, useRef } from 'react';
import React, { useCallback, useMemo, useRef } from 'react';
import type { DataView } from '@kbn/data-views-plugin/public';
import { generateFilters } from '@kbn/data-plugin/public';
import { ChildDragDropProvider, DragContext } from '@kbn/dom-drag-drop';
import { ChildDragDropProvider, useDragDropContext } from '@kbn/dom-drag-drop';
import {
UnifiedFieldListSidebarContainer,
type UnifiedFieldListSidebarContainerProps,
@ -54,7 +54,7 @@ export const FieldListSidebar: React.FC<FieldListSidebarProps> = ({
onAddFieldToWorkspace,
onRemoveFieldFromWorkspace,
}) => {
const dragDropContext = useContext(DragContext);
const dragDropContext = useDragDropContext();
const unifiedFieldListContainerRef = useRef<UnifiedFieldListSidebarContainerApi>(null);
const filterManager = services.data?.query?.filterManager;
@ -80,7 +80,7 @@ export const FieldListSidebar: React.FC<FieldListSidebarProps> = ({
}, [unifiedFieldListContainerRef]);
return (
<ChildDragDropProvider {...dragDropContext}>
<ChildDragDropProvider value={dragDropContext}>
<UnifiedFieldListSidebarContainer
ref={unifiedFieldListContainerRef}
variant="responsive"

View file

@ -9,7 +9,7 @@ We aren't using EUI or another library, due to the fact that Lens visualizations
First, place a RootDragDropProvider at the root of your application.
```js
<RootDragDropProvider onTrackUICounterEvent={...}>
<RootDragDropProvider customMiddleware={...}>
... your app here ...
</RootDragDropProvider>
```
@ -17,13 +17,13 @@ First, place a RootDragDropProvider at the root of your application.
If you have a child React application (e.g. a visualization), you will need to pass the drag / drop context down into it. This can be obtained like so:
```js
const context = useContext(DragContext);
const context = useDragDropContext();
```
In your child application, place a `ChildDragDropProvider` at the root of that, and spread the context into it:
In your child application, place a `ChildDragDropProvider` at the root of that, and assign the context into it:
```js
<ChildDragDropProvider {...context}>... your child app here ...</ChildDragDropProvider>
<ChildDragDropProvider value={context}>... your child app here ...</ChildDragDropProvider>
```
This enables your child application to share the same drag / drop context as the root application.
@ -49,7 +49,7 @@ To enable dragging an item, use `DragDrop` with both a `draggable` and a `value`
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.
```js
const { dragging } = useContext(DragContext);
const [ dndState ] = useDragDropContext()
return (
<DragDrop
@ -69,13 +69,13 @@ return (
To create a reordering group, surround the elements from the same group with a `ReorderProvider`:
```js
<ReorderProvider id="groupId">... elements from one group here ...</ReorderProvider>
<ReorderProvider>... elements from one group here ...</ReorderProvider>
```
The children `DragDrop` components must have props defined as in the example:
```js
<ReorderProvider id="groupId">
<ReorderProvider>
<div className="field-list">
{fields.map((f) => (
<DragDrop

View file

@ -8,16 +8,18 @@
export {
type DragDropIdentifier,
type DragContextValue,
type DragContextState,
type DropType,
type DraggingIdentifier,
type DragDropAction,
type DropOverlayWrapperProps,
DragDrop,
DragContext,
useDragDropContext,
RootDragDropProvider,
ChildDragDropProvider,
ReorderProvider,
DropOverlayWrapper,
type DropOverlayWrapperProps,
} from './src';
export { DropTargetSwapDuplicateCombine } from './src/drop_targets';

File diff suppressed because it is too large Load diff

View file

@ -14,14 +14,14 @@ import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect';
import {
DragDropIdentifier,
DropIdentifier,
DragContext,
DragContextState,
nextValidDropTarget,
ReorderContext,
ReorderState,
DropHandler,
announce,
Ghost,
RegisteredDropTargets,
DragDropAction,
DragContextState,
useDragDropContext,
} from './providers';
import { DropType } from './types';
import { REORDER_ITEM_MARGIN } from './constants';
@ -63,11 +63,6 @@ interface BaseProps {
*/
value: DragDropIdentifier;
/**
* Optional comparison function to check whether a value is the dragged one
*/
isValueEqual?: (value1: unknown, value2: unknown) => boolean;
/**
* The React element which will be passed the draggable handlers
*/
@ -125,17 +120,13 @@ interface BaseProps {
* The props for a draggable instance of that component.
*/
interface DragInnerProps extends BaseProps {
setKeyboardMode: DragContextState['setKeyboardMode'];
setDragging: DragContextState['setDragging'];
setActiveDropTarget: DragContextState['setActiveDropTarget'];
setA11yMessage: DragContextState['setA11yMessage'];
dndDispatch: React.Dispatch<DragDropAction>;
dataTestSubjPrefix?: string;
activeDraggingProps?: {
keyboardMode: DragContextState['keyboardMode'];
activeDropTarget: DragContextState['activeDropTarget'];
dropTargetsByOrder: DragContextState['dropTargetsByOrder'];
keyboardMode: boolean;
activeDropTarget?: DropIdentifier;
dropTargetsByOrder: RegisteredDropTargets;
};
dataTestSubjPrefix: DragContextState['dataTestSubjPrefix'];
onTrackUICounterEvent: DragContextState['onTrackUICounterEvent'] | undefined;
extraKeyboardHandler?: (e: KeyboardEvent<HTMLButtonElement>) => void;
ariaDescribedBy?: string;
}
@ -144,16 +135,8 @@ interface DragInnerProps extends BaseProps {
* The props for a non-draggable instance of that component.
*/
interface DropsInnerProps extends BaseProps {
dragging: DragContextState['dragging'];
keyboardMode: DragContextState['keyboardMode'];
setKeyboardMode: DragContextState['setKeyboardMode'];
setDragging: DragContextState['setDragging'];
setActiveDropTarget: DragContextState['setActiveDropTarget'];
setA11yMessage: DragContextState['setA11yMessage'];
registerDropTarget: DragContextState['registerDropTarget'];
activeDropTarget: DragContextState['activeDropTarget'];
dataTestSubjPrefix: DragContextState['dataTestSubjPrefix'];
onTrackUICounterEvent: DragContextState['onTrackUICounterEvent'] | undefined;
dndState: DragContextState;
dndDispatch: React.Dispatch<DragDropAction>;
isNotDroppable: boolean;
}
@ -165,19 +148,9 @@ const REORDER_OFFSET = REORDER_ITEM_MARGIN / 2;
* @constructor
*/
export const DragDrop = (props: BaseProps) => {
const {
dragging,
setDragging,
keyboardMode,
registerDropTarget,
dropTargetsByOrder,
setKeyboardMode,
activeDropTarget,
setActiveDropTarget,
setA11yMessage,
dataTestSubjPrefix,
onTrackUICounterEvent,
} = useContext(DragContext);
const [dndState, dndDispatch] = useDragDropContext();
const { dragging, dropTargetsByOrder } = dndState;
if (props.isDisabled) {
return props.children;
@ -188,8 +161,8 @@ export const DragDrop = (props: BaseProps) => {
const activeDraggingProps = isDragging
? {
keyboardMode,
activeDropTarget,
keyboardMode: dndState.keyboardMode,
activeDropTarget: dndState.activeDropTarget,
dropTargetsByOrder,
}
: undefined;
@ -198,12 +171,8 @@ export const DragDrop = (props: BaseProps) => {
const dragProps = {
...props,
activeDraggingProps,
setKeyboardMode,
setDragging,
setActiveDropTarget,
setA11yMessage,
dataTestSubjPrefix,
onTrackUICounterEvent,
dataTestSubjPrefix: dndState.dataTestSubjPrefix,
dndDispatch,
};
if (reorderableGroup && reorderableGroup.length > 1) {
return <ReorderableDrag {...dragProps} reorderableGroup={reorderableGroup} />;
@ -214,16 +183,8 @@ export const DragDrop = (props: BaseProps) => {
const dropProps = {
...props,
keyboardMode,
setKeyboardMode,
dragging,
setDragging,
activeDropTarget,
setActiveDropTarget,
registerDropTarget,
setA11yMessage,
dataTestSubjPrefix,
onTrackUICounterEvent,
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
@ -253,39 +214,35 @@ const DragInner = memo(function DragInner({
className,
value,
children,
setDragging,
setKeyboardMode,
setActiveDropTarget,
dndDispatch,
order,
activeDraggingProps,
dataTestSubjPrefix,
dragType,
onDragStart,
onDragEnd,
extraKeyboardHandler,
ariaDescribedBy,
setA11yMessage,
dataTestSubjPrefix,
onTrackUICounterEvent,
}: DragInnerProps) {
const keyboardMode = activeDraggingProps?.keyboardMode;
const activeDropTarget = activeDraggingProps?.activeDropTarget;
const dropTargetsByOrder = activeDraggingProps?.dropTargetsByOrder;
const { keyboardMode, activeDropTarget, dropTargetsByOrder } = activeDraggingProps || {};
const setTarget = useCallback(
(target?: DropIdentifier, announceModifierKeys = false) => {
setActiveDropTarget(target);
setA11yMessage(
target
? announce.selectedTarget(
value.humanData,
target?.humanData,
target?.dropType,
announceModifierKeys
)
: announce.noTarget()
);
(target?: DropIdentifier) => {
if (!target) {
dndDispatch({
type: 'leaveDropTarget',
});
} else {
dndDispatch({
type: 'selectDropTarget',
payload: {
dropTarget: target,
dragging: value,
},
});
}
},
[setActiveDropTarget, setA11yMessage, value.humanData]
[dndDispatch, value]
);
const setTargetOfIndex = useCallback(
@ -293,11 +250,7 @@ const DragInner = memo(function DragInner({
const dropTargetsForActiveId =
dropTargetsByOrder &&
Object.values(dropTargetsByOrder).filter((dropTarget) => dropTarget?.id === id);
if (index > 0 && dropTargetsForActiveId?.[index]) {
setTarget(dropTargetsForActiveId[index]);
} else {
setTarget(dropTargetsForActiveId?.[0], true);
}
setTarget(dropTargetsForActiveId?.[index]);
},
[dropTargetsByOrder, setTarget]
);
@ -339,58 +292,64 @@ const DragInner = memo(function DragInner({
return { onKeyDown, onKeyUp };
}, [activeDropTarget, setTargetOfIndex]);
const dragStart = (
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;
}
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);
}
// 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.
// 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;
const currentTarget = e?.currentTarget;
setTimeout(() => {
setDragging({
...value,
ghost: keyboardModeOn
? {
children,
style: { width: currentTarget.offsetWidth, minHeight: currentTarget?.offsetHeight },
}
: undefined,
setTimeout(() => {
dndDispatch({
type: 'startDragging',
payload: {
...(keyboardModeOn ? { keyboardMode: true } : {}),
dragging: {
...value,
ghost: keyboardModeOn
? {
children,
style: {
width: currentTarget.offsetWidth,
minHeight: currentTarget?.offsetHeight,
},
}
: undefined,
},
},
});
onDragStart?.(currentTarget);
});
setA11yMessage(announce.lifted(value.humanData));
if (keyboardModeOn) {
setKeyboardMode(true);
}
if (onDragStart) {
onDragStart(currentTarget);
}
});
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[dndDispatch, value, onDragStart]
);
const dragEnd = (e?: DroppableEvent) => {
e?.stopPropagation();
setDragging(undefined);
setActiveDropTarget(undefined);
setKeyboardMode(false);
setA11yMessage(announce.cancelled(value.humanData));
if (onDragEnd) {
onDragEnd();
}
};
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(
@ -408,16 +367,23 @@ const DragInner = memo(function DragInner({
} else if (e.ctrlKey && nextTarget?.id) {
setTargetOfIndex(nextTarget.id, 3);
} else {
setTarget(nextTarget, true);
setTarget(nextTarget);
}
};
const dropToActiveDropTarget = () => {
if (activeDropTarget) {
onTrackUICounterEvent?.('drop_total');
const { dropType, humanData, onDrop: onTargetDrop } = activeDropTarget;
setTimeout(() => setA11yMessage(announce.dropped(value.humanData, humanData, dropType)));
onTargetDrop(value, dropType);
const { dropType, onDrop } = activeDropTarget;
setTimeout(() => {
dndDispatch({
type: 'dropToTarget',
payload: {
dragging: value,
dropTarget: activeDropTarget,
},
});
});
onDrop(value, dropType);
}
};
@ -501,38 +467,35 @@ const DropsInner = memo(function DropsInner(props: DropsInnerProps) {
value,
children,
draggable,
dragging,
dndState,
dndDispatch,
isNotDroppable,
dropTypes,
order,
getAdditionalClassesOnEnter,
getAdditionalClassesOnDroppable,
activeDropTarget,
registerDropTarget,
setActiveDropTarget,
keyboardMode,
setKeyboardMode,
setDragging,
setA11yMessage,
getCustomDropTarget,
dataTestSubjPrefix,
} = props;
const { dragging, activeDropTarget, dataTestSubjPrefix, keyboardMode } = dndState;
const [isInZone, setIsInZone] = useState(false);
const mainTargetRef = useRef<HTMLDivElement>(null);
useShallowCompareEffect(() => {
if (dropTypes && dropTypes?.[0] && onDrop && keyboardMode) {
dropTypes.forEach((dropType, index) => {
registerDropTarget([...order, index], { ...value, onDrop, dropType });
dndDispatch({
type: 'registerDropTargets',
payload: dropTypes.reduce(
(acc, dropType, index) => ({
...acc,
[[...props.order, index].join(',')]: { ...value, onDrop, dropType },
}),
{}
),
});
return () => {
dropTypes.forEach((_, index) => {
registerDropTarget([...order, index], undefined);
});
};
}
}, [order, registerDropTarget, dropTypes, keyboardMode]);
}, [order, dndDispatch, dropTypes, keyboardMode]);
useEffect(() => {
let isMounted = true;
@ -586,15 +549,18 @@ const DropsInner = memo(function DropsInner(props: DropsInnerProps) {
);
// An optimization to prevent a bunch of React churn.
if (!isActiveDropTarget) {
setActiveDropTarget({ ...value, dropType: modifiedDropType, onDrop });
setA11yMessage(
announce.selectedTarget(dragging.humanData, value.humanData, modifiedDropType)
);
dndDispatch({
type: 'selectDropTarget',
payload: {
dropTarget: { ...value, dropType: modifiedDropType, onDrop },
dragging,
},
});
}
};
const dragLeave = () => {
setActiveDropTarget(undefined);
dndDispatch({ type: 'leaveDropTarget' });
};
const drop = (e: DroppableEvent, dropType: DropType) => {
@ -604,14 +570,17 @@ const DropsInner = memo(function DropsInner(props: DropsInnerProps) {
if (onDrop && dragging) {
const modifiedDropType = getModifiedDropType(e, dropType);
onDrop(dragging, modifiedDropType);
setTimeout(() =>
setA11yMessage(announce.dropped(dragging.humanData, value.humanData, modifiedDropType))
);
setTimeout(() => {
dndDispatch({
type: 'dropToTarget',
payload: {
dragging,
dropTarget: { ...value, dropType: modifiedDropType, onDrop },
},
});
});
}
setDragging(undefined);
setActiveDropTarget(undefined);
setKeyboardMode(false);
dndDispatch({ type: 'resetState' });
};
const getProps = (dropType?: DropType, dropChildren?: ReactElement) => {
@ -746,23 +715,11 @@ const SingleDropInner = ({
const ReorderableDrag = memo(function ReorderableDrag(
props: DragInnerProps & { reorderableGroup: Array<{ id: string }>; dragging?: DragDropIdentifier }
) {
const {
reorderState: { isReorderOn, reorderedItems, direction },
setReorderState,
} = useContext(ReorderContext);
const [{ isReorderOn, reorderedItems, direction }, reorderDispatch] = useContext(ReorderContext);
const {
value,
setActiveDropTarget,
activeDraggingProps,
reorderableGroup,
setA11yMessage,
dataTestSubjPrefix,
} = props;
const { value, activeDraggingProps, reorderableGroup, dndDispatch, dataTestSubjPrefix } = props;
const keyboardMode = activeDraggingProps?.keyboardMode;
const activeDropTarget = activeDraggingProps?.activeDropTarget;
const dropTargetsByOrder = activeDraggingProps?.dropTargetsByOrder;
const { keyboardMode, activeDropTarget, dropTargetsByOrder } = activeDraggingProps || {};
const isDragging = !!activeDraggingProps;
const isFocusInGroup = keyboardMode
@ -771,11 +728,11 @@ const ReorderableDrag = memo(function ReorderableDrag(
: isDragging;
useEffect(() => {
setReorderState((s: ReorderState) => ({
...s,
isReorderOn: isFocusInGroup,
}));
}, [setReorderState, isFocusInGroup]);
reorderDispatch({
type: 'setIsReorderOn',
payload: isFocusInGroup,
});
}, [reorderDispatch, isFocusInGroup]);
const onReorderableDragStart = (
currentTarget?:
@ -783,24 +740,19 @@ const ReorderableDrag = memo(function ReorderableDrag(
| KeyboardEvent<HTMLButtonElement>['currentTarget']
) => {
if (currentTarget) {
const height = currentTarget.offsetHeight + REORDER_OFFSET;
setReorderState((s: ReorderState) => ({
...s,
draggingHeight: height,
}));
setTimeout(() => {
reorderDispatch({
type: 'registerDraggingItemHeight',
payload: currentTarget.offsetHeight + REORDER_OFFSET,
});
});
}
};
const onReorderableDragEnd = () => {
resetReorderState();
reorderDispatch({ type: 'reset' });
};
const resetReorderState = () =>
setReorderState((s: ReorderState) => ({
...s,
reorderedItems: [],
}));
const extraKeyboardHandler = (e: KeyboardEvent<HTMLButtonElement>) => {
if (isReorderOn && keyboardMode) {
e.stopPropagation();
@ -811,8 +763,7 @@ const ReorderableDrag = memo(function ReorderableDrag(
if (index !== -1) activeDropTargetIndex = index;
}
if (e.key === keys.ARROW_LEFT || e.key === keys.ARROW_RIGHT) {
resetReorderState();
setActiveDropTarget(undefined);
reorderDispatch({ type: 'reset' });
} else if (keys.ARROW_DOWN === e.key) {
if (activeDropTargetIndex < reorderableGroup.length - 1) {
const nextTarget = nextValidDropTarget(
@ -840,12 +791,8 @@ const ReorderableDrag = memo(function ReorderableDrag(
const onReorderableDragOver = (target?: DropIdentifier) => {
if (!target) {
setReorderState((s: ReorderState) => ({
...s,
reorderedItems: [],
}));
setA11yMessage(announce.selectedTarget(value.humanData, value.humanData, 'reorder'));
setActiveDropTarget(target);
reorderDispatch({ type: 'reset' });
dndDispatch({ type: 'leaveDropTarget' });
return;
}
const droppingIndex = reorderableGroup.findIndex((i) => i.id === target.id);
@ -853,40 +800,34 @@ const ReorderableDrag = memo(function ReorderableDrag(
if (draggingIndex === -1) {
return;
}
setActiveDropTarget(target);
setA11yMessage(announce.selectedTarget(value.humanData, target.humanData, 'reorder'));
setReorderState((s: ReorderState) =>
draggingIndex < droppingIndex
? {
...s,
reorderedItems: reorderableGroup.slice(draggingIndex + 1, droppingIndex + 1),
direction: '-',
}
: {
...s,
reorderedItems: reorderableGroup.slice(droppingIndex, draggingIndex),
direction: '+',
}
);
dndDispatch({
type: 'selectDropTarget',
payload: {
dropTarget: target,
dragging: value,
},
});
reorderDispatch({
type: 'setReorderedItems',
payload: { draggingIndex, droppingIndex, items: reorderableGroup },
});
};
const areItemsReordered = isDragging && keyboardMode && reorderedItems.length;
const areItemsReordered = keyboardMode && isDragging && reorderedItems.length;
return (
<div
data-test-subj={`${dataTestSubjPrefix}-reorderableDrag`}
className={
isDragging
? 'domDragDrop-reorderable domDragDrop-translatableDrag'
: 'domDragDrop-reorderable'
}
className={classNames('domDragDrop-reorderable', {
['domDragDrop-translatableDrag']: isDragging,
['domDragDrop-isKeyboardReorderInProgress']: keyboardMode && isDragging,
})}
style={
areItemsReordered
? {
transform: `translateY(${direction === '+' ? '-' : '+'}${reorderedItems.reduce(
(acc, cur) => acc + Number(cur.height || 0) + REORDER_OFFSET,
(acc, el) => acc + (el.height ?? 0) + REORDER_OFFSET,
0
)}px)`,
}
@ -907,26 +848,13 @@ const ReorderableDrag = memo(function ReorderableDrag(
const ReorderableDrop = memo(function ReorderableDrop(
props: DropsInnerProps & { reorderableGroup: Array<{ id: string }> }
) {
const {
onDrop,
value,
dragging,
setDragging,
setKeyboardMode,
activeDropTarget,
setActiveDropTarget,
reorderableGroup,
setA11yMessage,
dataTestSubjPrefix,
onTrackUICounterEvent,
} = props;
const { onDrop, value, dndState, dndDispatch, reorderableGroup } = props;
const { dragging, dataTestSubjPrefix, activeDropTarget } = dndState;
const currentIndex = reorderableGroup.findIndex((i) => i.id === value.id);
const {
reorderState: { isReorderOn, reorderedItems, draggingHeight, direction },
setReorderState,
} = useContext(ReorderContext);
const [{ isReorderOn, reorderedItems, draggingHeight, direction }, reorderDispatch] =
useContext(ReorderContext);
const heightRef = useRef<HTMLDivElement>(null);
@ -935,52 +863,38 @@ const ReorderableDrop = memo(function ReorderableDrop(
useEffect(() => {
if (isReordered && heightRef.current?.clientHeight) {
setReorderState((s) => ({
...s,
reorderedItems: s.reorderedItems.map((el) =>
el.id === value.id
? {
...el,
height: heightRef.current?.clientHeight,
}
: el
),
}));
reorderDispatch({
type: 'registerReorderedItemHeight',
payload: { id: value.id, height: heightRef.current.clientHeight },
});
}
}, [isReordered, setReorderState, value.id]);
}, [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) {
setActiveDropTarget({ ...value, dropType: 'reorder', onDrop });
const draggingIndex = reorderableGroup.findIndex((i) => i.id === dragging?.id);
if (!dragging || draggingIndex === -1) {
return;
}
const droppingIndex = currentIndex;
if (draggingIndex === droppingIndex) {
setReorderState((s: ReorderState) => ({
...s,
reorderedItems: [],
}));
reorderDispatch({ type: 'reset' });
}
setReorderState((s: ReorderState) =>
draggingIndex < droppingIndex
? {
...s,
reorderedItems: reorderableGroup.slice(draggingIndex + 1, droppingIndex + 1),
direction: '-',
}
: {
...s,
reorderedItems: reorderableGroup.slice(droppingIndex, draggingIndex),
direction: '+',
}
);
reorderDispatch({
type: 'setReorderedItems',
payload: { draggingIndex, droppingIndex, items: reorderableGroup },
});
dndDispatch({
type: 'selectDropTarget',
payload: {
dropTarget: { ...value, dropType: 'reorder', onDrop },
dragging,
},
});
}
};
@ -988,18 +902,20 @@ const ReorderableDrop = memo(function ReorderableDrop(
e.preventDefault();
e.stopPropagation();
setActiveDropTarget(undefined);
setDragging(undefined);
setKeyboardMode(false);
if (onDrop && dragging) {
onTrackUICounterEvent?.('drop_total');
onDrop(dragging, 'reorder');
// setTimeout ensures it will run after dragEnd messaging
setTimeout(() =>
setA11yMessage(announce.dropped(dragging.humanData, value.humanData, 'reorder'))
);
setTimeout(() => {
dndDispatch({
type: 'dropToTarget',
payload: {
dragging,
dropTarget: { ...value, dropType: 'reorder', onDrop },
},
});
});
}
dndDispatch({ type: 'resetState' });
};
return (
@ -1027,11 +943,8 @@ const ReorderableDrop = memo(function ReorderableDrop(
onDrop={onReorderableDrop}
onDragOver={onReorderableDragOver}
onDragLeave={() => {
setActiveDropTarget(undefined);
setReorderState((s: ReorderState) => ({
...s,
reorderedItems: [],
}));
dndDispatch({ type: 'leaveDropTarget' });
reorderDispatch({ type: 'reset' });
}}
/>
</div>

View file

@ -10,11 +10,7 @@ import { i18n } from '@kbn/i18n';
import { DropType } from '../types';
import { HumanData } from '.';
type AnnouncementFunction = (
draggedElement: HumanData,
dropElement: HumanData,
announceModifierKeys?: boolean
) => string;
type AnnouncementFunction = (draggedElement: HumanData, dropElement: HumanData) => string;
interface CustomAnnouncementsType {
dropped: Partial<{ [dropType in DropType]: AnnouncementFunction }>;
@ -32,10 +28,9 @@ const replaceAnnouncement = {
canDuplicate,
canCombine,
layerNumber: dropLayerNumber,
}: HumanData,
announceModifierKeys?: boolean
}: HumanData
) => {
if (announceModifierKeys && (canSwap || canDuplicate)) {
if (canSwap || canDuplicate) {
return i18n.translate('domDragDrop.announce.selectedTarget.replaceMain', {
defaultMessage: `You're dragging {label} from {groupLabel} at position {position} in layer {layerNumber} over {dropLabel} from {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Press space or enter to replace {dropLabel} with {label}.{duplicateCopy}{swapCopy}{combineCopy}`,
values: {
@ -168,10 +163,9 @@ const combineAnnouncement = {
canDuplicate,
canCombine,
layerNumber: dropLayerNumber,
}: HumanData,
announceModifierKeys?: boolean
}: HumanData
) => {
if (announceModifierKeys && (canSwap || canDuplicate || canCombine)) {
if (canSwap || canDuplicate || canCombine) {
return i18n.translate('domDragDrop.announce.selectedTarget.combineMain', {
defaultMessage: `You're dragging {label} from {groupLabel} at position {position} in layer {layerNumber} over {dropLabel} from {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Press space or enter to combine {dropLabel} with {label}.{duplicateCopy}{swapCopy}{combineCopy}`,
values: {
@ -247,10 +241,9 @@ export const announcements: CustomAnnouncementsType = {
canDuplicate,
canCombine,
layerNumber: dropLayerNumber,
}: HumanData,
announceModifierKeys?: boolean
}: HumanData
) => {
if (announceModifierKeys && (canSwap || canDuplicate || canCombine)) {
if (canSwap || canDuplicate || canCombine) {
return i18n.translate('domDragDrop.announce.selectedTarget.replaceIncompatibleMain', {
defaultMessage: `You're dragging {label} from {groupLabel} at position {position} in layer {layerNumber} over {dropLabel} from {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Press space or enter to convert {label} to {nextLabel} and replace {dropLabel}.{duplicateCopy}{swapCopy}{combineCopy}`,
values: {
@ -290,10 +283,9 @@ export const announcements: CustomAnnouncementsType = {
canSwap,
canDuplicate,
layerNumber: dropLayerNumber,
}: HumanData,
announceModifierKeys?: boolean
}: HumanData
) => {
if (announceModifierKeys && (canSwap || canDuplicate)) {
if (canSwap || canDuplicate) {
return i18n.translate('domDragDrop.announce.selectedTarget.moveIncompatibleMain', {
defaultMessage: `You're dragging {label} from {groupLabel} at position {position} in layer {layerNumber} over position {dropPosition} in {dropGroupLabel} group in layer {dropLayerNumber}. Press space or enter to convert {label} to {nextLabel} and move.{duplicateCopy}`,
values: {
@ -329,10 +321,9 @@ export const announcements: CustomAnnouncementsType = {
canSwap,
canDuplicate,
layerNumber: dropLayerNumber,
}: HumanData,
announceModifierKeys?: boolean
}: HumanData
) => {
if (announceModifierKeys && (canSwap || canDuplicate)) {
if (canSwap || canDuplicate) {
return i18n.translate('domDragDrop.announce.selectedTarget.moveCompatibleMain', {
defaultMessage: `You're dragging {label} from {groupLabel} at position {position} over position {dropPosition} in {dropGroupLabel} group in layer {dropLayerNumber}. Press space or enter to move.{duplicateCopy}`,
values: {
@ -790,19 +781,9 @@ export const announce = {
dropped: (draggedElement: HumanData, dropElement: HumanData, type?: DropType) =>
(type && announcements.dropped?.[type]?.(draggedElement, dropElement)) ||
defaultAnnouncements.dropped(draggedElement, dropElement),
selectedTarget: (
draggedElement: HumanData,
dropElement: HumanData,
type?: DropType,
announceModifierKeys?: boolean
) => {
selectedTarget: (draggedElement: HumanData, dropElement: HumanData, type?: DropType) => {
return (
(type &&
announcements.selectedTarget?.[type]?.(
draggedElement,
dropElement,
announceModifierKeys
)) ||
(type && announcements.selectedTarget?.[type]?.(draggedElement, dropElement)) ||
defaultAnnouncements.selectedTarget(draggedElement, dropElement)
);
},

View file

@ -6,9 +6,9 @@
* Side Public License, v 1.
*/
import React, { useContext } from 'react';
import React from 'react';
import { mount } from 'enzyme';
import { RootDragDropProvider, DragContext } from '.';
import { RootDragDropProvider, useDragDropContext } from '.';
jest.useFakeTimers({ legacyFakeTimers: true });
@ -16,11 +16,11 @@ describe('RootDragDropProvider', () => {
test('reuses contexts for each render', () => {
const contexts: Array<{}> = [];
const TestComponent = ({ name }: { name: string }) => {
const context = useContext(DragContext);
const context = useDragDropContext();
contexts.push(context);
return (
<div data-test-subj="test-component">
{name} {!!context.dragging}
{name} {!!context[0].dragging}
</div>
);
};

View file

@ -6,45 +6,54 @@
* Side Public License, v 1.
*/
import React, { useState, useMemo } from 'react';
import React, { Reducer, useReducer } from 'react';
import { EuiScreenReaderOnly } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
DropIdentifier,
DraggingIdentifier,
DragDropIdentifier,
RegisteredDropTargets,
DragContextValue,
DragContextState,
CustomMiddleware,
DraggingIdentifier,
} from './types';
import { DEFAULT_DATA_TEST_SUBJ } from '../constants';
import { announce } from './announcements';
const initialState = {
dragging: undefined,
activeDropTarget: undefined,
keyboardMode: false,
dropTargetsByOrder: {},
dataTestSubjPrefix: DEFAULT_DATA_TEST_SUBJ,
};
/**
* The drag / drop context singleton, used like so:
*
* const { dragging, setDragging } = useContext(DragContext);
* const [ state, dispatch ] = useDragDropContext();
*/
export const DragContext = React.createContext<DragContextState>({
dragging: undefined,
setDragging: () => {},
keyboardMode: false,
setKeyboardMode: () => {},
activeDropTarget: undefined,
setActiveDropTarget: () => {},
setA11yMessage: () => {},
dropTargetsByOrder: undefined,
registerDropTarget: () => {},
dataTestSubjPrefix: DEFAULT_DATA_TEST_SUBJ,
onTrackUICounterEvent: undefined,
});
const DragContext = React.createContext<DragContextValue>([initialState, () => {}]);
export function useDragDropContext() {
const context = React.useContext(DragContext);
if (context === undefined) {
throw new Error(
'useDragDropContext must be used within a <RootDragDropProvider/> or <ChildDragDropProvider/>'
);
}
return context;
}
/**
* The argument to DragDropProvider.
*/
export interface ProviderProps extends DragContextState {
export interface ProviderProps {
/**
* The React children.
*/
children: React.ReactNode;
value: DragContextValue;
}
/**
@ -54,74 +63,193 @@ export interface ProviderProps extends DragContextState {
*
* @param props
*/
interface ResetStateAction {
type: 'resetState';
payload?: string;
}
interface EndDraggingAction {
type: 'endDragging';
payload: {
dragging: DraggingIdentifier;
};
}
interface StartDraggingAction {
type: 'startDragging';
payload: {
dragging: DraggingIdentifier;
keyboardMode?: boolean;
};
}
interface LeaveDropTargetAction {
type: 'leaveDropTarget';
}
interface SelectDropTargetAction {
type: 'selectDropTarget';
payload: {
dropTarget: DropIdentifier;
dragging: DragDropIdentifier;
};
}
interface DragToTargetAction {
type: 'dropToTarget';
payload: {
dragging: DragDropIdentifier;
dropTarget: DropIdentifier;
};
}
interface RegisterDropTargetAction {
type: 'registerDropTargets';
payload: RegisteredDropTargets;
}
export type DragDropAction =
| ResetStateAction
| RegisterDropTargetAction
| LeaveDropTargetAction
| SelectDropTargetAction
| DragToTargetAction
| StartDraggingAction
| EndDraggingAction;
const dragDropReducer = (state: DragContextState, action: DragDropAction) => {
switch (action.type) {
case 'resetState':
case 'endDragging':
return {
...state,
dropTargetsByOrder: undefined,
dragging: undefined,
keyboardMode: false,
activeDropTarget: undefined,
};
case 'registerDropTargets':
return {
...state,
dropTargetsByOrder: {
...state.dropTargetsByOrder,
...action.payload,
},
};
case 'dropToTarget':
return {
...state,
dropTargetsByOrder: undefined,
dragging: undefined,
keyboardMode: false,
activeDropTarget: undefined,
};
case 'leaveDropTarget':
return {
...state,
activeDropTarget: undefined,
};
case 'selectDropTarget':
return {
...state,
activeDropTarget: action.payload.dropTarget,
};
case 'startDragging':
return {
...state,
...action.payload,
};
default:
return state;
}
};
const useReducerWithMiddleware = (
reducer: Reducer<DragContextState, DragDropAction>,
initState: DragContextState,
middlewareFns?: Array<(action: DragDropAction) => void>
) => {
const [state, dispatch] = useReducer(reducer, initState);
const dispatchWithMiddleware = React.useCallback(
(action: DragDropAction) => {
if (middlewareFns !== undefined && middlewareFns.length > 0) {
middlewareFns.forEach((middlewareFn) => middlewareFn(action));
}
dispatch(action);
},
[middlewareFns]
);
return [state, dispatchWithMiddleware] as const;
};
const useA11yMiddleware = () => {
const [a11yMessage, setA11yMessage] = React.useState('');
const a11yMiddleware = React.useCallback((action: DragDropAction) => {
switch (action.type) {
case 'startDragging':
setA11yMessage(announce.lifted(action.payload.dragging.humanData));
return;
case 'selectDropTarget':
setA11yMessage(
announce.selectedTarget(
action.payload.dragging.humanData,
action.payload.dropTarget.humanData,
action.payload.dropTarget.dropType
)
);
return;
case 'leaveDropTarget':
setA11yMessage(announce.noTarget());
return;
case 'dropToTarget':
const { dragging, dropTarget } = action.payload;
setA11yMessage(
announce.dropped(dragging.humanData, dropTarget.humanData, dropTarget.dropType)
);
return;
case 'endDragging':
setA11yMessage(announce.cancelled(action.payload.dragging.humanData));
return;
default:
return;
}
}, []);
return { a11yMessage, a11yMiddleware };
};
export function RootDragDropProvider({
children,
dataTestSubj = DEFAULT_DATA_TEST_SUBJ,
onTrackUICounterEvent,
customMiddleware,
}: {
children: React.ReactNode;
dataTestSubj?: string;
onTrackUICounterEvent?: DragContextState['onTrackUICounterEvent'];
customMiddleware?: CustomMiddleware;
}) {
const [draggingState, setDraggingState] = useState<{ dragging?: DraggingIdentifier }>({
dragging: undefined,
});
const [keyboardModeState, setKeyboardModeState] = useState(false);
const [a11yMessageState, setA11yMessageState] = useState('');
const [activeDropTargetState, setActiveDropTargetState] = useState<DropIdentifier | undefined>(
undefined
);
const { a11yMessage, a11yMiddleware } = useA11yMiddleware();
const middlewareFns = React.useMemo(() => {
return customMiddleware ? [customMiddleware, a11yMiddleware] : [a11yMiddleware];
}, [customMiddleware, a11yMiddleware]);
const [dropTargetsByOrderState, setDropTargetsByOrderState] = useState<RegisteredDropTargets>({});
const setDragging = useMemo(
() => (dragging?: DraggingIdentifier) => setDraggingState({ dragging }),
[setDraggingState]
);
const setA11yMessage = useMemo(
() => (message: string) => setA11yMessageState(message),
[setA11yMessageState]
);
const setActiveDropTarget = useMemo(
() => (activeDropTarget?: DropIdentifier) => setActiveDropTargetState(activeDropTarget),
[setActiveDropTargetState]
);
const registerDropTarget = useMemo(
() => (order: number[], dropTarget?: DropIdentifier) => {
return setDropTargetsByOrderState((s) => {
return {
...s,
[order.join(',')]: dropTarget,
};
});
const [state, dispatch] = useReducerWithMiddleware(
dragDropReducer,
{
...initialState,
dataTestSubjPrefix: dataTestSubj,
},
[setDropTargetsByOrderState]
middlewareFns
);
return (
<>
<ChildDragDropProvider
keyboardMode={keyboardModeState}
setKeyboardMode={setKeyboardModeState}
dragging={draggingState.dragging}
setA11yMessage={setA11yMessage}
setDragging={setDragging}
activeDropTarget={activeDropTargetState}
setActiveDropTarget={setActiveDropTarget}
registerDropTarget={registerDropTarget}
dropTargetsByOrder={dropTargetsByOrderState}
dataTestSubjPrefix={dataTestSubj}
onTrackUICounterEvent={onTrackUICounterEvent}
>
{children}
</ChildDragDropProvider>
<ChildDragDropProvider value={[state, dispatch]}>{children}</ChildDragDropProvider>
<EuiScreenReaderOnly>
<div>
<p aria-live="assertive" aria-atomic={true}>
{a11yMessageState}
{a11yMessage}
</p>
<p id={`${dataTestSubj}-keyboardInstructionsWithReorder`}>
{i18n.translate('domDragDrop.keyboardInstructionsReorder', {
@ -206,47 +334,6 @@ export function nextValidDropTarget(
*
* @param props
*/
export function ChildDragDropProvider({
dragging,
setDragging,
setKeyboardMode,
keyboardMode,
activeDropTarget,
setActiveDropTarget,
setA11yMessage,
registerDropTarget,
dropTargetsByOrder,
dataTestSubjPrefix,
onTrackUICounterEvent,
children,
}: ProviderProps) {
const value = useMemo(
() => ({
setKeyboardMode,
keyboardMode,
dragging,
setDragging,
activeDropTarget,
setActiveDropTarget,
setA11yMessage,
dropTargetsByOrder,
registerDropTarget,
dataTestSubjPrefix,
onTrackUICounterEvent,
}),
[
setDragging,
dragging,
activeDropTarget,
setActiveDropTarget,
setKeyboardMode,
keyboardMode,
setA11yMessage,
dropTargetsByOrder,
registerDropTarget,
dataTestSubjPrefix,
onTrackUICounterEvent,
]
);
export function ChildDragDropProvider({ value, children }: ProviderProps) {
return <DragContext.Provider value={value}>{children}</DragContext.Provider>;
}

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import React, { useState, useMemo } from 'react';
import React, { useReducer, Reducer, Dispatch } from 'react';
import classNames from 'classnames';
import { DEFAULT_DATA_TEST_SUBJ, REORDER_ITEM_HEIGHT } from '../constants';
@ -31,69 +31,116 @@ export interface ReorderState {
* indicates that user is in keyboard mode
*/
isReorderOn: boolean;
/**
* reorder group needed for screen reader aria-described-by attribute
*/
groupId: string;
}
type SetReorderStateDispatch = (prevState: ReorderState) => ReorderState;
const initialState: ReorderState = {
reorderedItems: [],
direction: '-' as const,
draggingHeight: REORDER_ITEM_HEIGHT,
isReorderOn: false,
};
/**
* Reorder context state
*/
export interface ReorderContextState {
reorderState: ReorderState;
setReorderState: (dispatch: SetReorderStateDispatch) => void;
}
export type ReorderContextState = [ReorderState, Dispatch<ReorderAction>];
/**
* Reorder context
*/
export const ReorderContext = React.createContext<ReorderContextState>({
reorderState: {
reorderedItems: [],
direction: '-',
draggingHeight: REORDER_ITEM_HEIGHT,
isReorderOn: false,
groupId: '',
},
setReorderState: () => () => {},
});
export const ReorderContext = React.createContext<ReorderContextState>([
initialState,
() => () => {},
]);
/**
* To create a reordering group, surround the elements from the same group with a `ReorderProvider`
* @param id
* @param children
* @param className
* @param draggingHeight
* @param dataTestSubj
* @constructor
*/
interface ResetAction {
type: 'reset';
}
interface RegisterDraggingItemHeightAction {
type: 'registerDraggingItemHeight';
payload: number;
}
interface RegisterReorderedItemHeightAction {
type: 'registerReorderedItemHeight';
payload: { id: string; height: number };
}
interface SetIsReorderOnAction {
type: 'setIsReorderOn';
payload: boolean;
}
interface SetReorderedItemsAction {
type: 'setReorderedItems';
payload: {
items: ReorderState['reorderedItems'];
draggingIndex: number;
droppingIndex: number;
};
}
type ReorderAction =
| ResetAction
| RegisterDraggingItemHeightAction
| RegisterReorderedItemHeightAction
| SetIsReorderOnAction
| SetReorderedItemsAction;
const reorderReducer = (state: ReorderState, action: ReorderAction) => {
switch (action.type) {
case 'reset':
return { ...state, reorderedItems: [] };
case 'registerDraggingItemHeight':
return { ...state, draggingHeight: action.payload };
case 'registerReorderedItemHeight':
return {
...state,
reorderedItems: state.reorderedItems.map((i) =>
i.id === action.payload.id ? { ...i, height: action.payload.height } : i
),
};
case 'setIsReorderOn':
return { ...state, isReorderOn: action.payload };
case 'setReorderedItems':
const { items, draggingIndex, droppingIndex } = action.payload;
return draggingIndex < droppingIndex
? {
...state,
reorderedItems: items.slice(draggingIndex + 1, droppingIndex + 1),
direction: '-' as const,
}
: {
...state,
reorderedItems: items.slice(droppingIndex, draggingIndex),
direction: '+' as const,
};
default:
return state;
}
};
export function ReorderProvider({
id,
children,
className,
draggingHeight = REORDER_ITEM_HEIGHT,
dataTestSubj = DEFAULT_DATA_TEST_SUBJ,
}: {
id: string;
children: React.ReactNode;
className?: string;
draggingHeight?: number;
dataTestSubj?: string;
}) {
const [state, setState] = useState<ReorderContextState['reorderState']>({
reorderedItems: [],
direction: '-',
draggingHeight,
isReorderOn: false,
groupId: id,
});
const setReorderState = useMemo(
() => (dispatch: SetReorderStateDispatch) => setState(dispatch),
[setState]
const [state, dispatch] = useReducer<Reducer<ReorderState, ReorderAction>>(
reorderReducer,
initialState
);
return (
@ -103,9 +150,7 @@ export function ReorderProvider({
'domDragDrop-isActiveGroup': state.isReorderOn && React.Children.count(children) > 1,
})}
>
<ReorderContext.Provider value={{ reorderState: state, setReorderState }}>
{children}
</ReorderContext.Provider>
<ReorderContext.Provider value={[state, dispatch]}>{children}</ReorderContext.Provider>
</div>
);
}

View file

@ -7,6 +7,7 @@
*/
import { DropType } from '../types';
import { DragDropAction } from './providers';
export interface HumanData {
label: string;
@ -57,45 +58,34 @@ export type DropHandler = (dropped: DragDropIdentifier, dropType?: DropType) =>
export type RegisteredDropTargets = Record<string, DropIdentifier | undefined> | undefined;
/**
* The shape of the drag / drop context.
*/
export interface DragContextState {
/**
* The item being dragged or undefined.
*/
dragging?: DraggingIdentifier;
/**
* keyboard mode
*/
keyboardMode: boolean;
/**
* keyboard mode
* currently selected drop target
*/
setKeyboardMode: (mode: boolean) => void;
/**
* Set the item being dragged.
*/
setDragging: (dragging?: DraggingIdentifier) => void;
activeDropTarget?: DropIdentifier;
/**
* currently registered drop targets
*/
dropTargetsByOrder: RegisteredDropTargets;
setActiveDropTarget: (newTarget?: DropIdentifier) => void;
setA11yMessage: (message: string) => void;
registerDropTarget: (order: number[], dropTarget?: DropIdentifier) => void;
/**
* Customizable data-test-subj prefix
*/
dataTestSubjPrefix: string;
/**
* A custom callback for telemetry
* @param event
*/
onTrackUICounterEvent?: (event: string) => void;
}
export type CustomMiddleware = (action: DragDropAction) => void;
export type DragContextValue = [
state: DragContextState,
dispatch: React.Dispatch<DragDropAction>,
customMiddleware?: CustomMiddleware
];

View file

@ -62,6 +62,7 @@ export interface FieldItemButtonProps<T extends FieldListItem> {
* @param otherProps
* @constructor
*/
export function FieldItemButton<T extends FieldListItem = DataViewField>({
field,
fieldSearchHighlight,

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import './discover_layout.scss';
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
EuiButtonIcon,
EuiFlexGroup,
@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n';
import { METRIC_TYPE } from '@kbn/analytics';
import classNames from 'classnames';
import { generateFilters } from '@kbn/data-plugin/public';
import { DragContext } from '@kbn/dom-drag-drop';
import { useDragDropContext } from '@kbn/dom-drag-drop';
import { DataViewField, DataViewType } from '@kbn/data-views-plugin/public';
import { useSavedSearchInitial } from '../../services/discover_state_provider';
import { DiscoverStateContainer } from '../../services/discover_state';
@ -182,8 +182,8 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
const resizeRef = useRef<HTMLDivElement>(null);
const dragDropContext = useContext(DragContext);
const draggingFieldName = dragDropContext.dragging?.id;
const [{ dragging }] = useDragDropContext();
const draggingFieldName = dragging?.id;
const onDropFieldToTable = useMemo(() => {
if (!draggingFieldName || currentColumns.includes(draggingFieldName)) {

View file

@ -7,13 +7,18 @@
*/
import { css } from '@emotion/react';
import { DragContext, DragDrop, DropTargetSwapDuplicateCombine } from '@kbn/dom-drag-drop';
import {
DragDrop,
DropTargetSwapDuplicateCombine,
ReorderProvider,
useDragDropContext,
} from '@kbn/dom-drag-drop';
import {
DimensionButton,
DimensionTrigger,
EmptyDimensionButton,
} from '@kbn/visualization-ui-components/public';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { euiThemeVars } from '@kbn/ui-theme';
import { i18n } from '@kbn/i18n';
@ -34,8 +39,6 @@ export const AnnotationList = ({
setNewAnnotationId(uuidv4());
}, [annotations.length]);
const { dragging } = useContext(DragContext);
const addAnnotationText = i18n.translate('eventAnnotation.annotationList.add', {
defaultMessage: 'Add annotation',
});
@ -59,45 +62,78 @@ export const AnnotationList = ({
[annotations, newAnnotationId, selectAnnotation, updateAnnotations]
);
const reorderAnnotations = useCallback(
(
sourceAnnotation: EventAnnotationConfig | undefined,
targetAnnotation: EventAnnotationConfig
) => {
if (!sourceAnnotation || sourceAnnotation.id === targetAnnotation.id) {
return annotations;
}
const newAnnotations = annotations.filter((c) => c.id !== sourceAnnotation.id);
const targetPosition = newAnnotations.findIndex((c) => c.id === targetAnnotation.id);
const targetIndex = annotations.indexOf(sourceAnnotation);
const sourceIndex = annotations.indexOf(targetAnnotation);
newAnnotations.splice(
targetIndex < sourceIndex ? targetPosition + 1 : targetPosition,
0,
sourceAnnotation
);
return updateAnnotations(newAnnotations);
},
[annotations, updateAnnotations]
);
const [{ dragging }] = useDragDropContext();
return (
<div>
{annotations.map((annotation, index) => (
<div
key={index}
css={css`
margin-top: ${euiThemeVars.euiSizeS};
position: relative; // this is to properly contain the absolutely-positioned drop target in DragDrop
`}
>
<DragDrop
order={[index]}
key={annotation.id}
value={{
id: annotation.id,
humanData: {
label: annotation.label,
},
}}
dragType="copy"
dropTypes={[]}
draggable
<ReorderProvider>
{annotations.map((annotation, index) => (
<div
key={index}
css={css`
margin-top: ${euiThemeVars.euiSizeS};
position: relative; // this is to properly contain the absolutely-positioned drop target in DragDrop
`}
>
<DimensionButton
groupLabel={i18n.translate('eventAnnotation.groupEditor.addAnnotation', {
defaultMessage: 'Annotations',
})}
onClick={() => selectAnnotation(annotation)}
onRemoveClick={() =>
updateAnnotations(annotations.filter(({ id }) => id !== annotation.id))
}
accessorConfig={getAnnotationAccessor(annotation)}
label={annotation.label}
<DragDrop
order={[index]}
key={annotation.id}
value={{
id: annotation.id,
humanData: {
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);
}}
>
<DimensionTrigger label={annotation.label} />
</DimensionButton>
</DragDrop>
</div>
))}
<DimensionButton
groupLabel={i18n.translate('eventAnnotation.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>
</DragDrop>
</div>
))}
</ReorderProvider>
<div
css={css`
@ -110,7 +146,7 @@ export const AnnotationList = ({
getAdditionalClassesOnDroppable={
DropTargetSwapDuplicateCombine.getAdditionalClassesOnDroppable
}
dropTypes={dragging ? ['field_add'] : []}
dropTypes={dragging ? ['duplicate_compatible'] : []}
value={{
id: 'addAnnotation',
humanData: {

View file

@ -7,7 +7,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { createMockedDragDropContext } from './mocks';
import { createMockedDragDropContext } from '../../mocks';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import {
dataViewPluginMocks,
@ -336,7 +336,6 @@ describe('FormBased Data Panel', () => {
fieldFormats: fieldFormatsServiceMock.createStartContract(),
indexPatternFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(),
onIndexPatternRefresh: jest.fn(),
dragDropContext: createMockedDragDropContext(),
currentIndexPatternId: '1',
core,
dateRange: {
@ -387,10 +386,6 @@ describe('FormBased Data Panel', () => {
currentIndexPatternId: '',
}}
setState={jest.fn()}
dragDropContext={{
...createMockedDragDropContext(),
dragging: { id: '1', humanData: { label: 'Label' } },
}}
frame={createMockFramePublicAPI()}
/>
);
@ -413,10 +408,9 @@ describe('FormBased Data Panel', () => {
dataViews,
}),
setState: jest.fn(),
dragDropContext: {
...createMockedDragDropContext(),
dragDropContext: createMockedDragDropContext({
dragging: { id: '1', humanData: { label: 'Label' } },
},
}),
dateRange: { fromDate: '2019-01-01', toDate: '2020-01-01' },
frame: getFrameAPIMock({
indexPatterns: indexPatterns as unknown as DataViewsState['indexPatterns'],

View file

@ -27,7 +27,6 @@ import {
useExistingFieldsFetcher,
useGroupedFields,
} from '@kbn/unified-field-list';
import { ChildDragDropProvider, DragContextState } from '@kbn/dom-drag-drop';
import { ChartsPluginSetup } from '@kbn/charts-plugin/public';
import type {
DatasourceDataPanelProps,
@ -41,7 +40,7 @@ import { FieldItem } from '../common/field_item';
export type Props = Omit<
DatasourceDataPanelProps<FormBasedPrivateState>,
'core' | 'onChangeIndexPattern'
'core' | 'onChangeIndexPattern' | 'dragDropContext'
> & {
data: DataPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
@ -77,7 +76,6 @@ function onSupportedFieldFilter(field: IndexPatternField): boolean {
export function FormBasedDataPanel({
state,
dragDropContext,
core,
data,
dataViews,
@ -144,7 +142,6 @@ export function FormBasedDataPanel({
query={query}
dateRange={dateRange}
filters={filters}
dragDropContext={dragDropContext}
core={core}
data={data}
dataViews={dataViews}
@ -171,7 +168,6 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
query,
dateRange,
filters,
dragDropContext,
core,
data,
dataViews,
@ -187,14 +183,13 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
activeIndexPatterns,
}: Omit<
DatasourceDataPanelProps,
'state' | 'setState' | 'core' | 'onChangeIndexPattern' | 'usedIndexPatterns'
'state' | 'setState' | 'core' | 'onChangeIndexPattern' | 'usedIndexPatterns' | 'dragDropContext'
> & {
data: DataPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
fieldFormats: FieldFormatsStart;
core: CoreStart;
currentIndexPatternId: string;
dragDropContext: DragContextState;
charts: ChartsPluginSetup;
frame: FramePublicAPI;
indexPatternFieldEditor: IndexPatternFieldEditorStart;
@ -398,20 +393,18 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
);
return (
<ChildDragDropProvider {...dragDropContext}>
<FieldList
className="lnsInnerIndexPatternDataPanel"
isProcessing={isProcessing}
prepend={<FieldListFilters {...fieldListFiltersProps} data-test-subj="lnsIndexPattern" />}
>
<FieldListGrouped<IndexPatternField>
{...fieldListGroupedProps}
renderFieldItem={renderFieldItem}
data-test-subj="lnsIndexPattern"
localStorageKeyPrefix="lens"
/>
</FieldList>
</ChildDragDropProvider>
<FieldList
className="lnsInnerIndexPatternDataPanel"
isProcessing={isProcessing}
prepend={<FieldListFilters {...fieldListFiltersProps} data-test-subj="lnsIndexPattern" />}
>
<FieldListGrouped<IndexPatternField>
{...fieldListGroupedProps}
renderFieldItem={renderFieldItem}
data-test-subj="lnsIndexPattern"
localStorageKeyPrefix="lens"
/>
</FieldList>
);
};

View file

@ -25,8 +25,9 @@ import { FormattedMessage } from '@kbn/i18n-react';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import { EuiButton } from '@elastic/eui';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { DraggingIdentifier } from '@kbn/dom-drag-drop';
import { ChildDragDropProvider, type DraggingIdentifier } from '@kbn/dom-drag-drop';
import { DimensionTrigger } from '@kbn/visualization-ui-components/public';
import memoizeOne from 'memoize-one';
import type {
DatasourceDimensionEditorProps,
DatasourceDimensionTriggerProps,
@ -72,7 +73,7 @@ import {
cloneLayer,
getNotifiableFeatures,
} from './utils';
import { getUniqueLabelGenerator, isDraggedDataViewField } from '../../utils';
import { getUniqueLabelGenerator, isDraggedDataViewField, nonNullable } from '../../utils';
import { hasField, normalizeOperationDataType } from './pure_utils';
import { LayerPanel } from './layerpanel';
import {
@ -112,6 +113,20 @@ function wrapOnDot(str?: string) {
return str ? str.replace(/\./g, '.\u200B') : '';
}
const getSelectedFieldsFromColumns = memoizeOne(
(columns: GenericIndexPatternColumn[]) =>
columns
.flatMap((c) => {
if (operationDefinitionMap[c.operationType]?.getCurrentFields) {
return operationDefinitionMap[c.operationType]?.getCurrentFields?.(c) || [];
} else if ('sourceField' in c) {
return c.sourceField;
}
})
.filter(nonNullable),
isEqual
);
function getSortingHint(column: GenericIndexPatternColumn, dataView?: IndexPattern | DataView) {
if (column.dataType === 'string') {
const fieldTypes =
@ -421,18 +436,9 @@ export function getFormBasedDatasource({
},
getSelectedFields(state) {
const fields: string[] = [];
Object.values(state?.layers)?.forEach((l) => {
const { columns } = l;
Object.values(columns).forEach((c) => {
if (operationDefinitionMap[c.operationType]?.getCurrentFields) {
fields.push(...(operationDefinitionMap[c.operationType]?.getCurrentFields?.(c) || []));
} else if ('sourceField' in c) {
fields.push(c.sourceField);
}
});
});
return fields;
return getSelectedFieldsFromColumns(
Object.values(state?.layers)?.flatMap((l) => Object.values(l.columns))
);
},
toExpression: (state, layerId, indexPatterns, dateRange, nowInstant, searchSessionId) =>
@ -470,7 +476,7 @@ export function getFormBasedDatasource({
},
renderDataPanel(domElement: Element, props: DatasourceDataPanelProps<FormBasedPrivateState>) {
const { onChangeIndexPattern, ...otherProps } = props;
const { onChangeIndexPattern, dragDropContext, ...otherProps } = props;
const layerFields = formBasedDatasource?.getSelectedFields?.(props.state);
render(
@ -487,18 +493,20 @@ export function getFormBasedDatasource({
share,
}}
>
<FormBasedDataPanel
data={data}
dataViews={dataViews}
fieldFormats={fieldFormats}
charts={charts}
indexPatternFieldEditor={dataViewFieldEditor}
{...otherProps}
core={core}
uiActions={uiActions}
onIndexPatternRefresh={onRefreshIndexPattern}
layerFields={layerFields}
/>
<ChildDragDropProvider value={dragDropContext}>
<FormBasedDataPanel
data={data}
dataViews={dataViews}
fieldFormats={fieldFormats}
charts={charts}
indexPatternFieldEditor={dataViewFieldEditor}
{...otherProps}
core={core}
uiActions={uiActions}
onIndexPatternRefresh={onRefreshIndexPattern}
layerFields={layerFields}
/>
</ChildDragDropProvider>
</KibanaContextProvider>
</I18nProvider>
</KibanaThemeProvider>,

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { DragContextState } from '@kbn/dom-drag-drop';
import { getFieldByNameFactory } from './pure_helpers';
import type { IndexPattern, IndexPatternField } from '../../types';
@ -217,18 +216,3 @@ export const createMockedIndexPatternWithoutType = (
getFieldByName: getFieldByNameFactory(filteredFields),
};
};
export function createMockedDragDropContext(): jest.Mocked<DragContextState> {
return {
dataTestSubjPrefix: 'lnsDragDrop',
dragging: undefined,
setDragging: jest.fn(),
activeDropTarget: undefined,
setActiveDropTarget: jest.fn(),
keyboardMode: false,
setKeyboardMode: jest.fn(),
setA11yMessage: jest.fn(),
dropTargetsByOrder: undefined,
registerDropTarget: jest.fn(),
};
}

View file

@ -28,8 +28,7 @@ import { mountWithIntl } from '@kbn/test-jest-helpers';
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
import { createIndexPatternServiceMock } from '../../mocks/data_views_service_mock';
import { createMockFramePublicAPI } from '../../mocks';
import { createMockedDragDropContext } from './mocks';
import { createMockFramePublicAPI, createMockedDragDropContext } from '../../mocks';
import { DataViewsState } from '../../state_management';
const fieldsFromQuery = [

View file

@ -24,7 +24,6 @@ import {
GetCustomFieldType,
useGroupedFields,
} from '@kbn/unified-field-list';
import { ChildDragDropProvider } from '@kbn/dom-drag-drop';
import type { DatasourceDataPanelProps } from '../../types';
import type { TextBasedPrivateState } from './types';
import { getStateFromAggregateQuery } from './utils';
@ -42,7 +41,6 @@ export type TextBasedDataPanelProps = DatasourceDataPanelProps<TextBasedPrivateS
export function TextBasedDataPanel({
setState,
state,
dragDropContext,
core,
data,
query,
@ -54,7 +52,7 @@ export function TextBasedDataPanel({
layerFields,
hasSuggestionForField,
dropOntoWorkspace,
}: TextBasedDataPanelProps) {
}: Omit<TextBasedDataPanelProps, 'dragDropContext'>) {
const prevQuery = usePrevious(query);
const [dataHasLoaded, setDataHasLoaded] = useState(false);
useEffect(() => {
@ -138,22 +136,20 @@ export function TextBasedDataPanel({
...core,
}}
>
<ChildDragDropProvider {...dragDropContext}>
<FieldList
className="lnsInnerIndexPatternDataPanel"
isProcessing={!dataHasLoaded}
prepend={
<FieldListFilters {...fieldListFiltersProps} data-test-subj="lnsTextBasedLanguages" />
}
>
<FieldListGrouped<DatatableColumn>
{...fieldListGroupedProps}
renderFieldItem={renderFieldItem}
data-test-subj="lnsTextBasedLanguages"
localStorageKeyPrefix="lens"
/>
</FieldList>
</ChildDragDropProvider>
<FieldList
className="lnsInnerIndexPatternDataPanel"
isProcessing={!dataHasLoaded}
prepend={
<FieldListFilters {...fieldListFiltersProps} data-test-subj="lnsTextBasedLanguages" />
}
>
<FieldListGrouped<DatatableColumn>
{...fieldListGroupedProps}
renderFieldItem={renderFieldItem}
data-test-subj="lnsTextBasedLanguages"
localStorageKeyPrefix="lens"
/>
</FieldList>
</KibanaContextProvider>
);
}

View file

@ -1,23 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { DragContextState } from '@kbn/dom-drag-drop';
export function createMockedDragDropContext(): jest.Mocked<DragContextState> {
return {
dataTestSubjPrefix: 'lnsDragDrop',
dragging: undefined,
setDragging: jest.fn(),
activeDropTarget: undefined,
setActiveDropTarget: jest.fn(),
keyboardMode: false,
setKeyboardMode: jest.fn(),
setA11yMessage: jest.fn(),
dropTargetsByOrder: undefined,
registerDropTarget: jest.fn(),
};
}

View file

@ -20,6 +20,9 @@ import type { DataViewsPublicPluginStart, DataView } from '@kbn/data-views-plugi
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { euiThemeVars } from '@kbn/ui-theme';
import { DimensionTrigger } from '@kbn/visualization-ui-components/public';
import memoizeOne from 'memoize-one';
import { isEqual } from 'lodash';
import { ChildDragDropProvider } from '@kbn/dom-drag-drop';
import {
DatasourceDimensionEditorProps,
DatasourceDataPanelProps,
@ -43,12 +46,24 @@ import type {
import { FieldSelect } from './field_select';
import type { Datasource, IndexPatternMap } from '../../types';
import { LayerPanel } from './layerpanel';
import { getUniqueLabelGenerator } from '../../utils';
import { getUniqueLabelGenerator, nonNullable } from '../../utils';
function getLayerReferenceName(layerId: string) {
return `textBasedLanguages-datasource-layer-${layerId}`;
}
const getSelectedFieldsFromColumns = memoizeOne(
(columns: TextBasedLayerColumn[]) =>
columns
.map((c) => {
if ('fieldName' in c) {
return c.fieldName;
}
})
.filter(nonNullable),
isEqual
);
export function getTextBasedDatasource({
core,
storage,
@ -344,30 +359,26 @@ export function getTextBasedDatasource({
return toExpression(state, layerId);
},
getSelectedFields(state) {
const fields: string[] = [];
Object.values(state?.layers)?.forEach((l) => {
const { columns } = l;
Object.values(columns).forEach((c) => {
if ('fieldName' in c) {
fields.push(c.fieldName);
}
});
});
return fields;
return getSelectedFieldsFromColumns(
Object.values(state?.layers)?.flatMap((l) => Object.values(l.columns))
);
},
renderDataPanel(domElement: Element, props: DatasourceDataPanelProps<TextBasedPrivateState>) {
const layerFields = TextBasedDatasource?.getSelectedFields?.(props.state);
const { dragDropContext, ...otherProps } = props;
render(
<KibanaThemeProvider theme$={core.theme.theme$}>
<I18nProvider>
<TextBasedDataPanel
data={data}
dataViews={dataViews}
expressions={expressions}
layerFields={layerFields}
{...props}
/>
<ChildDragDropProvider value={dragDropContext}>
<TextBasedDataPanel
data={data}
dataViews={dataViews}
expressions={expressions}
layerFields={layerFields}
{...otherProps}
/>
</ChildDragDropProvider>
</I18nProvider>
</KibanaThemeProvider>,
domElement

View file

@ -5,11 +5,11 @@
* 2.0.
*/
import React, { useMemo, useCallback, useContext, ReactElement } from 'react';
import React, { useMemo, useCallback, ReactElement } from 'react';
import {
DragDrop,
DragDropIdentifier,
DragContext,
useDragDropContext,
DropType,
DropTargetSwapDuplicateCombine,
} from '@kbn/dom-drag-drop';
@ -61,7 +61,7 @@ export function DraggableDimensionButton({
registerNewButtonRef: (id: string, instance: HTMLDivElement | null) => void;
indexPatterns: IndexPatternMap;
}) {
const { dragging } = useContext(DragContext);
const [{ dragging }] = useDragDropContext();
let getDropProps;
@ -139,20 +139,20 @@ export function DraggableDimensionButton({
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}
reorderableGroup={reorderableGroup.length > 1 ? reorderableGroup : undefined}
value={value}
onDrop={handleOnDrop}
onDragStart={() => onDragStart()}
onDragEnd={() => onDragEnd()}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
>
{children}
</DragDrop>

View file

@ -5,13 +5,13 @@
* 2.0.
*/
import React, { useMemo, useState, useEffect, useContext } from 'react';
import React, { useMemo, useState, useEffect } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import {
DragDrop,
DragDropIdentifier,
DragContext,
useDragDropContext,
DropType,
DropTargetSwapDuplicateCombine,
} from '@kbn/dom-drag-drop';
@ -116,7 +116,7 @@ export function EmptyDimensionButton({
};
};
}) {
const { dragging } = useContext(DragContext);
const [{ dragging }] = useDragDropContext();
let getDropProps;

View file

@ -20,6 +20,7 @@ import {
createMockDatasource,
DatasourceMock,
mountWithProvider,
createMockedDragDropContext,
} from '../../../mocks';
import { createIndexPatternServiceMock } from '../../../mocks/data_views_service_mock';
import { DimensionButton } from '@kbn/visualization-ui-components/public';
@ -52,19 +53,6 @@ afterEach(() => {
container = undefined;
});
const defaultContext = {
dataTestSubjPrefix: 'lnsDragDrop',
dragging: undefined,
setDragging: jest.fn(),
setActiveDropTarget: () => {},
activeDropTarget: undefined,
dropTargetsByOrder: undefined,
keyboardMode: false,
setKeyboardMode: () => {},
setA11yMessage: jest.fn(),
registerDropTarget: jest.fn(),
};
const draggingField = {
field: { name: 'dragged' },
indexPatternId: 'a',
@ -773,7 +761,7 @@ describe('LayerPanel', () => {
});
const { instance } = await mountWithProvider(
<ChildDragDropProvider {...defaultContext} dragging={draggingField}>
<ChildDragDropProvider value={createMockedDragDropContext({ dragging: draggingField })}>
<LayerPanel {...getDefaultProps()} />
</ChildDragDropProvider>
);
@ -817,7 +805,7 @@ describe('LayerPanel', () => {
);
const { instance } = await mountWithProvider(
<ChildDragDropProvider {...defaultContext} dragging={draggingField}>
<ChildDragDropProvider value={createMockedDragDropContext({ dragging: draggingField })}>
<LayerPanel {...getDefaultProps()} />
</ChildDragDropProvider>
);
@ -886,7 +874,7 @@ describe('LayerPanel', () => {
};
const { instance } = await mountWithProvider(
<ChildDragDropProvider {...defaultContext} dragging={draggingOperation}>
<ChildDragDropProvider value={createMockedDragDropContext({ dragging: draggingOperation })}>
<LayerPanel {...getDefaultProps()} />
</ChildDragDropProvider>
);
@ -956,7 +944,7 @@ describe('LayerPanel', () => {
};
const { instance } = await mountWithProvider(
<ChildDragDropProvider {...defaultContext} dragging={draggingOperation}>
<ChildDragDropProvider value={createMockedDragDropContext({ dragging: draggingOperation })}>
<LayerPanel {...getDefaultProps()} />
</ChildDragDropProvider>,
undefined,
@ -1009,7 +997,7 @@ describe('LayerPanel', () => {
};
const { instance } = await mountWithProvider(
<ChildDragDropProvider {...defaultContext} dragging={draggingOperation}>
<ChildDragDropProvider value={createMockedDragDropContext({ dragging: draggingOperation })}>
<LayerPanel {...getDefaultProps()} />
</ChildDragDropProvider>
);
@ -1064,7 +1052,7 @@ describe('LayerPanel', () => {
const mockOnRemoveDimension = jest.fn();
const { instance } = await mountWithProvider(
<ChildDragDropProvider {...defaultContext} dragging={draggingOperation}>
<ChildDragDropProvider value={createMockedDragDropContext({ dragging: draggingOperation })}>
<LayerPanel
{...getDefaultProps()}
onRemoveDimension={mockOnRemoveDimension}
@ -1139,7 +1127,7 @@ describe('LayerPanel', () => {
const mockOnRemoveDimension = jest.fn();
const { instance } = await mountWithProvider(
<ChildDragDropProvider {...defaultContext} dragging={draggingOperation}>
<ChildDragDropProvider value={createMockedDragDropContext({ dragging: draggingOperation })}>
<LayerPanel
{...getDefaultProps()}
onRemoveDimension={mockOnRemoveDimension}
@ -1221,7 +1209,7 @@ describe('LayerPanel', () => {
const mockOnRemoveDimension = jest.fn();
const { instance } = await mountWithProvider(
<ChildDragDropProvider {...defaultContext} dragging={draggingOperation}>
<ChildDragDropProvider value={createMockedDragDropContext({ dragging: draggingOperation })}>
<LayerPanel
{...getDefaultProps()}
onRemoveDimension={mockOnRemoveDimension}
@ -1284,7 +1272,7 @@ describe('LayerPanel', () => {
const mockOnRemoveDimension = jest.fn();
const { instance } = await mountWithProvider(
<ChildDragDropProvider {...defaultContext} dragging={draggingOperation}>
<ChildDragDropProvider value={createMockedDragDropContext({ dragging: draggingOperation })}>
<LayerPanel
{...getDefaultProps()}
onRemoveDimension={mockOnRemoveDimension}

View file

@ -547,11 +547,7 @@ export function LayerPanel(
>
<>
{group.accessors.length ? (
<ReorderProvider
id={group.groupId}
className={'lnsLayerPanel__group'}
dataTestSubj="lnsDragDrop"
>
<ReorderProvider className={'lnsLayerPanel__group'} dataTestSubj="lnsDragDrop">
{group.accessors.map((accessorConfig, accessorIndex) => {
const { columnId } = accessorConfig;
@ -644,7 +640,6 @@ export function LayerPanel(
{group.fakeFinalAccessor && (
<div
className="domDragDrop-isDraggable"
css={css`
display: flex;
align-items: center;

View file

@ -7,12 +7,14 @@
import './data_panel_wrapper.scss';
import React, { useMemo, memo, useContext, useEffect, useCallback } from 'react';
import React, { useMemo, memo, useEffect, useCallback } from 'react';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
import { DragContext, DragDropIdentifier } from '@kbn/dom-drag-drop';
import { useDragDropContext, DragDropIdentifier } from '@kbn/dom-drag-drop';
import memoizeOne from 'memoize-one';
import { isEqual } from 'lodash';
import { Easteregg } from './easteregg';
import { NativeRenderer } from '../../native_renderer';
import {
@ -54,6 +56,8 @@ interface DataPanelWrapperProps {
frame: FramePublicAPI;
}
const memoizeStrictlyEqual = memoizeOne((arg) => arg, isEqual);
export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
const externalContext = useLensSelector(selectExecutionContext);
const activeDatasourceId = useLensSelector(selectActiveDatasourceId);
@ -158,7 +162,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
const datasourceProps: DatasourceDataPanelProps = {
...externalContext,
dragDropContext: useContext(DragContext),
dragDropContext: useDragDropContext(),
state: activeDatasourceId ? datasourceStates[activeDatasourceId].state : null,
setState: setDatasourceState,
core: props.core,
@ -170,7 +174,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
indexPatternService: props.indexPatternService,
frame: props.frame,
// Visualization can handle dataViews, so need to pass to the data panel the full list of used dataViews
usedIndexPatterns: [
usedIndexPatterns: memoizeStrictlyEqual([
...((activeDatasourceId &&
props.datasourceMap[activeDatasourceId]?.getUsedDataViews(
datasourceStates[activeDatasourceId].state
@ -181,7 +185,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
visualizationState.state
)) ||
[]),
],
]),
};
return (

View file

@ -820,11 +820,16 @@ describe('editor_frame', () => {
getDatasourceSuggestionsForField: () => [generateSuggestion()],
getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()],
getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()],
renderDataPanel: (_element, { dragDropContext: { setDragging, dragging } }) => {
renderDataPanel: (_element, { dragDropContext: [{ dragging }, dndDispatch] }) => {
if (!dragging || dragging.id !== 'draggedField') {
setDragging({
id: 'draggedField',
humanData: { label: 'draggedField' },
dndDispatch({
type: 'startDragging',
payload: {
dragging: {
id: 'draggedField',
humanData: { label: 'draggedField' },
},
},
});
}
},
@ -922,11 +927,16 @@ describe('editor_frame', () => {
getDatasourceSuggestionsForField: () => [generateSuggestion()],
getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()],
getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()],
renderDataPanel: (_element, { dragDropContext: { setDragging, dragging } }) => {
renderDataPanel: (_element, { dragDropContext: [{ dragging }, dndDispatch] }) => {
if (!dragging || dragging.id !== 'draggedField') {
setDragging({
id: 'draggedField',
humanData: { label: '1' },
dndDispatch({
type: 'startDragging',
payload: {
dragging: {
id: 'draggedField',
humanData: { label: '1' },
},
},
});
}
},

View file

@ -8,7 +8,7 @@
import React, { useCallback, useRef } from 'react';
import { CoreStart } from '@kbn/core/public';
import { ReactExpressionRendererType } from '@kbn/expressions-plugin/public';
import { DragDropIdentifier, RootDragDropProvider } from '@kbn/dom-drag-drop';
import { type DragDropAction, DragDropIdentifier, RootDragDropProvider } from '@kbn/dom-drag-drop';
import { trackUiCounterEvents } from '../../lens_ui_telemetry';
import {
DatasourceMap,
@ -108,8 +108,14 @@ export function EditorFrame(props: EditorFrameProps) {
const bannerMessages = props.getUserMessages('banner', { severity: 'warning' });
const telemetryMiddleware = useCallback((action: DragDropAction) => {
if (action.type === 'dropToTarget') {
trackUiCounterEvents('drop_total');
}
}, []);
return (
<RootDragDropProvider dataTestSubj="lnsDragDrop" onTrackUICounterEvent={trackUiCounterEvents}>
<RootDragDropProvider dataTestSubj="lnsDragDrop" customMiddleware={telemetryMiddleware}>
<FrameLayout
bannerMessages={
bannerMessages.length ? (

View file

@ -15,6 +15,7 @@ import {
createExpressionRendererMock,
DatasourceMock,
createMockFramePublicAPI,
createMockedDragDropContext,
} from '../../../mocks';
import { mockDataPlugin, mountWithProvider } from '../../../mocks';
jest.mock('../../../debounced_component', () => {
@ -934,18 +935,7 @@ describe('workspace_panel', () => {
async function initComponent(draggingContext = draggedField) {
const mounted = await mountWithProvider(
<ChildDragDropProvider
dataTestSubjPrefix="lnsDragDrop"
dragging={draggingContext}
setDragging={() => {}}
setActiveDropTarget={() => {}}
activeDropTarget={undefined}
keyboardMode={false}
setKeyboardMode={() => {}}
setA11yMessage={() => {}}
registerDropTarget={jest.fn()}
dropTargetsByOrder={undefined}
>
<ChildDragDropProvider value={createMockedDragDropContext({ dragging: draggingContext })}>
<WorkspacePanel
{...defaultProps}
datasourceMap={{

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useState, useEffect, useMemo, useContext, useCallback, useRef } from 'react';
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import useObservable from 'react-use/lib/useObservable';
import classNames from 'classnames';
import { FormattedMessage } from '@kbn/i18n-react';
@ -34,7 +34,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, DragContext, DragDropIdentifier } from '@kbn/dom-drag-drop';
import { DragDrop, useDragDropContext, DragDropIdentifier } from '@kbn/dom-drag-drop';
import { trackUiCounterEvents } from '../../../lens_ui_telemetry';
import { getSearchWarningMessages } from '../../../utils';
import {
@ -126,11 +126,11 @@ const EXPRESSION_BUILD_ERROR_ID = 'expression_build_error';
export const WorkspacePanel = React.memo(function WorkspacePanel(props: WorkspacePanelProps) {
const { getSuggestionForField, ...restProps } = props;
const dragDropContext = useContext(DragContext);
const [{ dragging }] = useDragDropContext();
const suggestionForDraggedField = useMemo(
() => dragDropContext.dragging && getSuggestionForField(dragDropContext.dragging),
[dragDropContext.dragging, getSuggestionForField]
() => dragging && getSuggestionForField(dragging),
[dragging, getSuggestionForField]
);
return (
@ -573,15 +573,15 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
);
};
const dragDropContext = useContext(DragContext);
const [{ dragging }] = useDragDropContext();
const renderWorkspace = () => {
const customWorkspaceRenderer =
activeDatasourceId &&
datasourceMap[activeDatasourceId]?.getCustomWorkspaceRenderer &&
dragDropContext.dragging
dragging
? datasourceMap[activeDatasourceId].getCustomWorkspaceRenderer!(
datasourceStates[activeDatasourceId].state,
dragDropContext.dragging,
dragging,
dataViews.indexPatterns
)
: undefined;

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { DragContextState, DragContextValue } from '@kbn/dom-drag-drop';
import { createMockDataViewsState } from '../data_views_service/mocks';
import { FramePublicAPI, FrameDatasourceAPI } from '../types';
export { mockDataPlugin } from './data_plugin_mock';
@ -65,3 +66,20 @@ export const createMockFrameDatasourceAPI = ({
filters: filters ?? [],
dataViews: createMockDataViewsState(dataViews),
});
export function createMockedDragDropContext(
partialState?: Partial<DragContextState>,
setState?: jest.Mocked<DragContextValue>[1]
): jest.Mocked<DragContextValue> {
return [
{
dataTestSubjPrefix: 'lnsDragDrop',
dragging: undefined,
keyboardMode: false,
activeDropTarget: undefined,
dropTargetsByOrder: undefined,
...partialState,
},
setState ? setState : jest.fn(),
];
}

View file

@ -43,7 +43,7 @@ import { EventAnnotationGroupConfig } from '@kbn/event-annotation-plugin/common'
import type {
DraggingIdentifier,
DragDropIdentifier,
DragContextState,
DragContextValue,
DropType,
} from '@kbn/dom-drag-drop';
import type { AccessorConfig } from '@kbn/visualization-ui-components/public';
@ -588,7 +588,7 @@ export interface DatasourceLayerSettingsProps<T = unknown> {
export interface DatasourceDataPanelProps<T = unknown> {
state: T;
dragDropContext: DragContextState;
dragDropContext: DragContextValue;
setState: StateSetter<T, { applyImmediately?: boolean }>;
showNoDataPopover: () => void;
core: Pick<