mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[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:
parent
203c9b04b6
commit
91a0d2f454
33 changed files with 1208 additions and 1109 deletions
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
];
|
||||
|
|
|
@ -62,6 +62,7 @@ export interface FieldItemButtonProps<T extends FieldListItem> {
|
|||
* @param otherProps
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
export function FieldItemButton<T extends FieldListItem = DataViewField>({
|
||||
field,
|
||||
fieldSearchHighlight,
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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' },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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={{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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<
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue