mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
parent
f9c405e7b6
commit
d0c5726730
31 changed files with 2064 additions and 1117 deletions
|
@ -1,6 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DragDrop droppable is reflected in the className 1`] = `
|
||||
exports[`DragDrop defined dropType is reflected in the className 1`] = `
|
||||
<button
|
||||
class="lnsDragDrop lnsDragDrop-isDroppable lnsDragDrop-isDropTarget"
|
||||
data-test-subj="lnsDragDrop"
|
||||
|
@ -9,7 +9,7 @@ exports[`DragDrop droppable is reflected in the className 1`] = `
|
|||
</button>
|
||||
`;
|
||||
|
||||
exports[`DragDrop items that have droppable=false get special styling when another item is dragged 1`] = `
|
||||
exports[`DragDrop items that has dropType=undefined get special styling when another item is dragged 1`] = `
|
||||
<button
|
||||
className="lnsDragDrop lnsDragDrop-isDroppable lnsDragDrop-isNotDroppable"
|
||||
data-test-subj="lnsDragDrop"
|
||||
|
@ -22,10 +22,12 @@ exports[`DragDrop items that have droppable=false get special styling when anoth
|
|||
`;
|
||||
|
||||
exports[`DragDrop renders if nothing is being dragged 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-test-subj="lnsDragDrop_draggable-hello"
|
||||
>
|
||||
<button
|
||||
aria-describedby="lnsDragDrop-keyboardInstructions"
|
||||
aria-label="dragging"
|
||||
aria-label="hello"
|
||||
class="euiScreenReaderOnly--showOnFocus lnsDragDrop__keyboardHandler"
|
||||
data-test-subj="lnsDragDrop-keyboardHandler"
|
||||
/>
|
||||
|
|
180
x-pack/plugins/lens/public/drag_drop/announcements.tsx
Normal file
180
x-pack/plugins/lens/public/drag_drop/announcements.tsx
Normal file
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { DropType } from '../types';
|
||||
export interface HumanData {
|
||||
label: string;
|
||||
groupLabel?: string;
|
||||
position?: number;
|
||||
}
|
||||
|
||||
type AnnouncementFunction = (draggedElement: HumanData, dropElement: HumanData) => string;
|
||||
|
||||
interface CustomAnnouncementsType {
|
||||
dropped: Partial<{ [dropType in DropType]: AnnouncementFunction }>;
|
||||
selectedTarget: Partial<{ [dropType in DropType]: AnnouncementFunction }>;
|
||||
}
|
||||
|
||||
const selectedTargetReplace = (
|
||||
{ label }: HumanData,
|
||||
{ label: dropLabel, groupLabel, position }: HumanData
|
||||
) =>
|
||||
i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.replace', {
|
||||
defaultMessage: `Selected {dropLabel} in {groupLabel} group at position {position}. Press space or enter to replace {dropLabel} with {label}.`,
|
||||
values: {
|
||||
label,
|
||||
dropLabel,
|
||||
groupLabel,
|
||||
position,
|
||||
},
|
||||
});
|
||||
|
||||
const droppedReplace = ({ label }: HumanData, { label: dropLabel, groupLabel }: HumanData) =>
|
||||
i18n.translate('xpack.lens.dragDrop.announce.duplicated.replace', {
|
||||
defaultMessage:
|
||||
'You have dropped the item. You have replaced {dropLabel} with {label} in {groupLabel} group.',
|
||||
values: {
|
||||
label,
|
||||
dropLabel,
|
||||
groupLabel,
|
||||
},
|
||||
});
|
||||
|
||||
export const announcements: CustomAnnouncementsType = {
|
||||
selectedTarget: {
|
||||
reorder: ({ label, position: prevPosition }, { position }) =>
|
||||
prevPosition === position
|
||||
? i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.reorderedBack', {
|
||||
defaultMessage: `You have moved the item {label} back to position {prevPosition}`,
|
||||
values: {
|
||||
label,
|
||||
prevPosition,
|
||||
},
|
||||
})
|
||||
: i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.reordered', {
|
||||
defaultMessage: `You have moved the item {label} from position {prevPosition} to position {position}`,
|
||||
values: {
|
||||
label,
|
||||
position,
|
||||
prevPosition,
|
||||
},
|
||||
}),
|
||||
duplicate_in_group: ({ label }, { label: dropLabel, groupLabel, position }) =>
|
||||
i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.duplicated', {
|
||||
defaultMessage: `Selected {dropLabel} in {groupLabel} group at position {position}. Press space or enter to duplicate {label}.`,
|
||||
values: {
|
||||
label,
|
||||
dropLabel,
|
||||
groupLabel,
|
||||
position,
|
||||
},
|
||||
}),
|
||||
field_replace: selectedTargetReplace,
|
||||
replace_compatible: selectedTargetReplace,
|
||||
replace_incompatible: selectedTargetReplace,
|
||||
},
|
||||
dropped: {
|
||||
reorder: ({ label, position: prevPosition }, { position }) =>
|
||||
i18n.translate('xpack.lens.dragDrop.announce.dropped.reordered', {
|
||||
defaultMessage:
|
||||
'You have dropped the item {label}. You have moved the item from position {prevPosition} to positon {position}',
|
||||
values: {
|
||||
label,
|
||||
position,
|
||||
prevPosition,
|
||||
},
|
||||
}),
|
||||
duplicate_in_group: ({ label }, { groupLabel, position }) =>
|
||||
i18n.translate('xpack.lens.dragDrop.announce.dropped.duplicated', {
|
||||
defaultMessage:
|
||||
'You have dropped the item. You have duplicated {label} in {groupLabel} group at position {position}',
|
||||
values: {
|
||||
label,
|
||||
groupLabel,
|
||||
position,
|
||||
},
|
||||
}),
|
||||
field_replace: droppedReplace,
|
||||
replace_compatible: droppedReplace,
|
||||
replace_incompatible: droppedReplace,
|
||||
},
|
||||
};
|
||||
|
||||
const defaultAnnouncements = {
|
||||
lifted: ({ label }: HumanData) =>
|
||||
i18n.translate('xpack.lens.dragDrop.announce.lifted', {
|
||||
defaultMessage: `Lifted {label}`,
|
||||
values: {
|
||||
label,
|
||||
},
|
||||
}),
|
||||
cancelled: () =>
|
||||
i18n.translate('xpack.lens.dragDrop.announce.cancelled', {
|
||||
defaultMessage: 'Movement cancelled',
|
||||
}),
|
||||
noTarget: () => {
|
||||
return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.noSelected', {
|
||||
defaultMessage: `No target selected. Use arrow keys to select a target.`,
|
||||
});
|
||||
},
|
||||
|
||||
dropped: (
|
||||
{ label }: HumanData,
|
||||
{ groupLabel: dropGroupLabel, position, label: dropLabel }: HumanData
|
||||
) =>
|
||||
dropGroupLabel && position
|
||||
? i18n.translate('xpack.lens.dragDrop.announce.droppedDefault', {
|
||||
defaultMessage:
|
||||
'You have dropped {label} to {dropLabel} in {dropGroupLabel} group at position {position}',
|
||||
values: {
|
||||
label,
|
||||
dropGroupLabel,
|
||||
position,
|
||||
dropLabel,
|
||||
},
|
||||
})
|
||||
: i18n.translate('xpack.lens.dragDrop.announce.droppedNoPosition', {
|
||||
defaultMessage: 'You have dropped {label} to {dropLabel}',
|
||||
values: {
|
||||
label,
|
||||
dropLabel,
|
||||
},
|
||||
}),
|
||||
selectedTarget: (
|
||||
{ label }: HumanData,
|
||||
{ label: dropLabel, groupLabel: dropGroupLabel, position }: HumanData
|
||||
) => {
|
||||
return dropGroupLabel && position
|
||||
? i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.default', {
|
||||
defaultMessage: `Selected {dropLabel} in {dropGroupLabel} group at position {position}. Press space or enter to drop {label}`,
|
||||
values: {
|
||||
dropLabel,
|
||||
label,
|
||||
dropGroupLabel,
|
||||
position,
|
||||
},
|
||||
})
|
||||
: i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.defaultNoPosition', {
|
||||
defaultMessage: `Selected {dropLabel}. Press space or enter to drop {label}`,
|
||||
values: {
|
||||
dropLabel,
|
||||
label,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const announce = {
|
||||
...defaultAnnouncements,
|
||||
dropped: (draggedElement: HumanData, dropElement: HumanData, type?: DropType) =>
|
||||
(type && announcements.dropped?.[type]?.(draggedElement, dropElement)) ||
|
||||
defaultAnnouncements.dropped(draggedElement, dropElement),
|
||||
selectedTarget: (draggedElement: HumanData, dropElement: HumanData, type?: DropType) =>
|
||||
(type && announcements.selectedTarget?.[type]?.(draggedElement, dropElement)) ||
|
||||
defaultAnnouncements.selectedTarget(draggedElement, dropElement),
|
||||
};
|
|
@ -2,6 +2,7 @@
|
|||
@import '../mixins';
|
||||
|
||||
.lnsDragDrop {
|
||||
user-select: none;
|
||||
transition: background-color $euiAnimSpeedFast ease-in-out, border-color $euiAnimSpeedFast ease-in-out;
|
||||
}
|
||||
|
||||
|
@ -27,6 +28,7 @@
|
|||
|
||||
// Drop area when there's an item being dragged
|
||||
.lnsDragDrop-isDropTarget {
|
||||
@include lnsDroppable;
|
||||
@include lnsDroppableActive;
|
||||
}
|
||||
|
||||
|
@ -52,6 +54,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
.lnsDragDrop-notCompatible {
|
||||
background-color: $euiColorHighlight;
|
||||
border: $euiBorderWidthThin dashed $euiBorderColor;
|
||||
&.lnsDragDrop-isActiveDropTarget {
|
||||
background-color: rgba(251, 208, 17, .25);
|
||||
border-color: $euiColorVis5;
|
||||
}
|
||||
}
|
||||
|
||||
.lnsDragDrop__container {
|
||||
position: relative;
|
||||
}
|
||||
|
@ -73,6 +84,7 @@
|
|||
transform: translateY(0);
|
||||
transition: transform $euiAnimSpeedFast ease-in-out;
|
||||
position: relative;
|
||||
z-index: $euiZLevel1;
|
||||
}
|
||||
|
||||
// Draggable item when it is moving
|
||||
|
|
|
@ -7,38 +7,40 @@
|
|||
|
||||
import React from 'react';
|
||||
import { render, mount } from 'enzyme';
|
||||
import { DragDrop, DropHandler } from './drag_drop';
|
||||
import { DragDrop } from './drag_drop';
|
||||
import {
|
||||
ChildDragDropProvider,
|
||||
DragContextState,
|
||||
ReorderProvider,
|
||||
DragDropIdentifier,
|
||||
ActiveDropTarget,
|
||||
DropTargets,
|
||||
} from './providers';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { DropType } from '../types';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
const defaultContext = {
|
||||
dragging: undefined,
|
||||
setDragging: jest.fn(),
|
||||
setActiveDropTarget: () => {},
|
||||
activeDropTarget: undefined,
|
||||
keyboardMode: false,
|
||||
setKeyboardMode: () => {},
|
||||
setA11yMessage: jest.fn(),
|
||||
};
|
||||
|
||||
const dataTransfer = {
|
||||
setData: jest.fn(),
|
||||
getData: jest.fn(),
|
||||
};
|
||||
|
||||
describe('DragDrop', () => {
|
||||
const value = { id: '1', label: 'hello' };
|
||||
const defaultContext = {
|
||||
dragging: undefined,
|
||||
setDragging: jest.fn(),
|
||||
setActiveDropTarget: jest.fn(),
|
||||
activeDropTarget: undefined,
|
||||
keyboardMode: false,
|
||||
setKeyboardMode: () => {},
|
||||
setA11yMessage: jest.fn(),
|
||||
registerDropTarget: jest.fn(),
|
||||
};
|
||||
|
||||
const value = { id: '1', humanData: { label: 'hello' } };
|
||||
test('renders if nothing is being dragged', () => {
|
||||
const component = render(
|
||||
<DragDrop value={value} draggable label="dragging">
|
||||
<DragDrop value={value} draggable order={[2, 0, 1, 0]}>
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
);
|
||||
|
@ -46,10 +48,10 @@ describe('DragDrop', () => {
|
|||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('dragover calls preventDefault if droppable is true', () => {
|
||||
test('dragover calls preventDefault if dropType is defined', () => {
|
||||
const preventDefault = jest.fn();
|
||||
const component = mount(
|
||||
<DragDrop droppable value={value}>
|
||||
<DragDrop dropType="field_add" value={value} order={[2, 0, 1, 0]}>
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
);
|
||||
|
@ -59,10 +61,10 @@ describe('DragDrop', () => {
|
|||
expect(preventDefault).toBeCalled();
|
||||
});
|
||||
|
||||
test('dragover does not call preventDefault if droppable is false', () => {
|
||||
test('dragover does not call preventDefault if dropType is undefined', () => {
|
||||
const preventDefault = jest.fn();
|
||||
const component = mount(
|
||||
<DragDrop value={value}>
|
||||
<DragDrop value={value} order={[2, 0, 1, 0]}>
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
);
|
||||
|
@ -75,9 +77,15 @@ describe('DragDrop', () => {
|
|||
test('dragstart sets dragging in the context', async () => {
|
||||
const setDragging = jest.fn();
|
||||
|
||||
const setA11yMessage = jest.fn();
|
||||
const component = mount(
|
||||
<ChildDragDropProvider {...defaultContext} dragging={value} setDragging={setDragging}>
|
||||
<DragDrop value={value} draggable={true} label="drag label">
|
||||
<ChildDragDropProvider
|
||||
{...defaultContext}
|
||||
dragging={value}
|
||||
setDragging={setDragging}
|
||||
setA11yMessage={setA11yMessage}
|
||||
>
|
||||
<DragDrop value={value} draggable={true} order={[2, 0, 1, 0]}>
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
</ChildDragDropProvider>
|
||||
|
@ -87,8 +95,9 @@ describe('DragDrop', () => {
|
|||
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(dataTransfer.setData).toBeCalledWith('text', 'drag label');
|
||||
expect(dataTransfer.setData).toBeCalledWith('text', 'hello');
|
||||
expect(setDragging).toBeCalledWith(value);
|
||||
expect(setA11yMessage).toBeCalledWith('Lifted hello');
|
||||
});
|
||||
|
||||
test('drop resets all the things', async () => {
|
||||
|
@ -100,10 +109,10 @@ describe('DragDrop', () => {
|
|||
const component = mount(
|
||||
<ChildDragDropProvider
|
||||
{...defaultContext}
|
||||
dragging={{ id: '2', label: 'hi' }}
|
||||
dragging={{ id: '2', humanData: { label: 'label1' } }}
|
||||
setDragging={setDragging}
|
||||
>
|
||||
<DragDrop onDrop={onDrop} droppable={true} value={value}>
|
||||
<DragDrop onDrop={onDrop} dropType="field_add" value={value} order={[2, 0, 1, 0]}>
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
</ChildDragDropProvider>
|
||||
|
@ -116,18 +125,22 @@ describe('DragDrop', () => {
|
|||
expect(preventDefault).toBeCalled();
|
||||
expect(stopPropagation).toBeCalled();
|
||||
expect(setDragging).toBeCalledWith(undefined);
|
||||
expect(onDrop).toBeCalledWith({ id: '2', label: 'hi' }, { id: '1', label: 'hello' });
|
||||
expect(onDrop).toBeCalledWith({ id: '2', humanData: { label: 'label1' } }, 'field_add');
|
||||
});
|
||||
|
||||
test('drop function is not called on droppable=false', async () => {
|
||||
test('drop function is not called on dropType undefined', async () => {
|
||||
const preventDefault = jest.fn();
|
||||
const stopPropagation = jest.fn();
|
||||
const setDragging = jest.fn();
|
||||
const onDrop = jest.fn();
|
||||
|
||||
const component = mount(
|
||||
<ChildDragDropProvider {...defaultContext} dragging={{ id: 'hi' }} setDragging={setDragging}>
|
||||
<DragDrop onDrop={onDrop} droppable={false} value={value}>
|
||||
<ChildDragDropProvider
|
||||
{...defaultContext}
|
||||
dragging={{ id: 'hi', humanData: { label: 'label1' } }}
|
||||
setDragging={setDragging}
|
||||
>
|
||||
<DragDrop onDrop={onDrop} dropType={undefined} value={value} order={[2, 0, 1, 0]}>
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
</ChildDragDropProvider>
|
||||
|
@ -143,14 +156,15 @@ describe('DragDrop', () => {
|
|||
expect(onDrop).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('droppable is reflected in the className', () => {
|
||||
test('defined dropType is reflected in the className', () => {
|
||||
const component = render(
|
||||
<DragDrop
|
||||
onDrop={(x: unknown) => {
|
||||
throw x;
|
||||
}}
|
||||
droppable
|
||||
dropType="field_add"
|
||||
value={value}
|
||||
order={[2, 0, 1, 0]}
|
||||
>
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
|
@ -159,13 +173,18 @@ describe('DragDrop', () => {
|
|||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('items that have droppable=false get special styling when another item is dragged', () => {
|
||||
test('items that has dropType=undefined get special styling when another item is dragged', () => {
|
||||
const component = mount(
|
||||
<ChildDragDropProvider {...defaultContext} dragging={value}>
|
||||
<DragDrop value={value} draggable={true} label="a">
|
||||
<DragDrop value={value} draggable={true} order={[2, 0, 1, 0]}>
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
<DragDrop onDrop={(x: unknown) => {}} droppable={false} value={{ id: '2' }}>
|
||||
<DragDrop
|
||||
order={[2, 0, 1, 0]}
|
||||
onDrop={(x: unknown) => {}}
|
||||
dropType={undefined}
|
||||
value={{ id: '2', humanData: { label: 'label2' } }}
|
||||
>
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
</ChildDragDropProvider>
|
||||
|
@ -175,30 +194,39 @@ describe('DragDrop', () => {
|
|||
});
|
||||
|
||||
test('additional styles are reflected in the className until drop', () => {
|
||||
let dragging: { id: '1' } | undefined;
|
||||
const getAdditionalClasses = jest.fn().mockReturnValue('additional');
|
||||
let dragging: { id: '1'; humanData: { label: 'label1' } } | undefined;
|
||||
const getAdditionalClassesOnEnter = jest.fn().mockReturnValue('additional');
|
||||
const getAdditionalClassesOnDroppable = jest.fn().mockReturnValue('droppable');
|
||||
const setA11yMessage = jest.fn();
|
||||
let activeDropTarget;
|
||||
|
||||
const component = mount(
|
||||
<ChildDragDropProvider
|
||||
{...defaultContext}
|
||||
dragging={dragging}
|
||||
setA11yMessage={setA11yMessage}
|
||||
setDragging={() => {
|
||||
dragging = { id: '1' };
|
||||
dragging = { id: '1', humanData: { label: 'label1' } };
|
||||
}}
|
||||
setActiveDropTarget={(val) => {
|
||||
activeDropTarget = { activeDropTarget: val };
|
||||
}}
|
||||
activeDropTarget={activeDropTarget}
|
||||
>
|
||||
<DragDrop value={{ label: 'ignored', id: '3' }} draggable={true} label="a">
|
||||
<DragDrop
|
||||
value={{ id: '3', humanData: { label: 'ignored' } }}
|
||||
draggable={true}
|
||||
order={[2, 0, 1, 0]}
|
||||
>
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
<DragDrop
|
||||
order={[2, 0, 1, 0]}
|
||||
value={value}
|
||||
onDrop={(x: unknown) => {}}
|
||||
droppable
|
||||
getAdditionalClassesOnEnter={getAdditionalClasses}
|
||||
dropType="field_add"
|
||||
getAdditionalClassesOnEnter={getAdditionalClassesOnEnter}
|
||||
getAdditionalClassesOnDroppable={getAdditionalClassesOnDroppable}
|
||||
>
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
|
@ -210,6 +238,7 @@ describe('DragDrop', () => {
|
|||
.first()
|
||||
.simulate('dragstart', { dataTransfer });
|
||||
jest.runAllTimers();
|
||||
expect(setA11yMessage).toBeCalledWith('Lifted ignored');
|
||||
|
||||
component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragover');
|
||||
component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('drop');
|
||||
|
@ -217,8 +246,9 @@ describe('DragDrop', () => {
|
|||
});
|
||||
|
||||
test('additional enter styles are reflected in the className until dragleave', () => {
|
||||
let dragging: { id: '1' } | undefined;
|
||||
let dragging: { id: '1'; humanData: { label: 'label1' } } | undefined;
|
||||
const getAdditionalClasses = jest.fn().mockReturnValue('additional');
|
||||
const getAdditionalClassesOnDroppable = jest.fn().mockReturnValue('droppable');
|
||||
const setActiveDropTarget = jest.fn();
|
||||
|
||||
const component = mount(
|
||||
|
@ -226,7 +256,7 @@ describe('DragDrop', () => {
|
|||
setA11yMessage={jest.fn()}
|
||||
dragging={dragging}
|
||||
setDragging={() => {
|
||||
dragging = { id: '1' };
|
||||
dragging = { id: '1', humanData: { label: 'label1' } };
|
||||
}}
|
||||
setActiveDropTarget={setActiveDropTarget}
|
||||
activeDropTarget={
|
||||
|
@ -234,15 +264,22 @@ describe('DragDrop', () => {
|
|||
}
|
||||
keyboardMode={false}
|
||||
setKeyboardMode={(keyboardMode) => true}
|
||||
registerDropTarget={jest.fn()}
|
||||
>
|
||||
<DragDrop value={{ label: 'ignored', id: '3' }} draggable={true} label="a">
|
||||
<DragDrop
|
||||
value={{ id: '3', humanData: { label: 'ignored' } }}
|
||||
draggable={true}
|
||||
order={[2, 0, 1, 0]}
|
||||
>
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
<DragDrop
|
||||
order={[2, 0, 1, 0]}
|
||||
value={value}
|
||||
onDrop={(x: unknown) => {}}
|
||||
droppable
|
||||
dropType="field_add"
|
||||
getAdditionalClassesOnEnter={getAdditionalClasses}
|
||||
getAdditionalClassesOnDroppable={getAdditionalClassesOnDroppable}
|
||||
>
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
|
@ -257,19 +294,137 @@ describe('DragDrop', () => {
|
|||
|
||||
component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragover');
|
||||
expect(component.find('.additional')).toHaveLength(1);
|
||||
|
||||
component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragleave');
|
||||
expect(setActiveDropTarget).toBeCalledWith(undefined);
|
||||
});
|
||||
|
||||
test('Keyboard navigation: User receives proper drop Targets highlighted when pressing arrow keys', () => {
|
||||
const onDrop = jest.fn();
|
||||
const setActiveDropTarget = jest.fn();
|
||||
const setA11yMessage = jest.fn();
|
||||
const items = [
|
||||
{
|
||||
draggable: true,
|
||||
value: {
|
||||
id: '1',
|
||||
humanData: { label: 'label1', position: 1 },
|
||||
},
|
||||
children: '1',
|
||||
order: [2, 0, 0, 0],
|
||||
},
|
||||
{
|
||||
draggable: true,
|
||||
dragType: 'move' as 'copy' | 'move',
|
||||
|
||||
value: {
|
||||
id: '2',
|
||||
|
||||
humanData: { label: 'label2', position: 1 },
|
||||
},
|
||||
onDrop,
|
||||
dropType: 'move_compatible' as DropType,
|
||||
order: [2, 0, 1, 0],
|
||||
},
|
||||
{
|
||||
draggable: true,
|
||||
dragType: 'move' as 'copy' | 'move',
|
||||
value: {
|
||||
id: '3',
|
||||
humanData: { label: 'label3', position: 1 },
|
||||
},
|
||||
onDrop,
|
||||
dropType: 'replace_compatible' as DropType,
|
||||
order: [2, 0, 2, 0],
|
||||
},
|
||||
{
|
||||
draggable: true,
|
||||
dragType: 'move' as 'copy' | 'move',
|
||||
value: {
|
||||
id: '4',
|
||||
humanData: { label: 'label4', position: 2 },
|
||||
},
|
||||
order: [2, 0, 2, 1],
|
||||
},
|
||||
];
|
||||
const component = mount(
|
||||
<ChildDragDropProvider
|
||||
{...{
|
||||
...defaultContext,
|
||||
dragging: items[0].value,
|
||||
setActiveDropTarget,
|
||||
setA11yMessage,
|
||||
activeDropTarget: {
|
||||
activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' },
|
||||
dropTargetsByOrder: {
|
||||
'2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' },
|
||||
'2,0,2,0': { ...items[2].value, onDrop, dropType: 'replace_compatible' },
|
||||
},
|
||||
},
|
||||
keyboardMode: true,
|
||||
}}
|
||||
>
|
||||
{items.map((props) => (
|
||||
<DragDrop {...props} key={props.value.id}>
|
||||
<div />
|
||||
</DragDrop>
|
||||
))}
|
||||
</ChildDragDropProvider>
|
||||
);
|
||||
const keyboardHandler = component
|
||||
.find('[data-test-subj="lnsDragDrop-keyboardHandler"]')
|
||||
.first()
|
||||
.simulate('focus');
|
||||
act(() => {
|
||||
keyboardHandler.simulate('keydown', { key: 'ArrowRight' });
|
||||
expect(setActiveDropTarget).toBeCalledWith({
|
||||
...items[2].value,
|
||||
onDrop,
|
||||
dropType: items[2].dropType,
|
||||
});
|
||||
keyboardHandler.simulate('keydown', { key: 'Enter' });
|
||||
expect(setA11yMessage).toBeCalledWith(
|
||||
'Selected label3 in group at position 1. Press space or enter to replace label3 with label1.'
|
||||
);
|
||||
expect(setActiveDropTarget).toBeCalledWith(undefined);
|
||||
expect(onDrop).toBeCalledWith(
|
||||
{ humanData: { label: 'label1', position: 1 }, id: '1' },
|
||||
'move_compatible'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reordering', () => {
|
||||
const onDrop = jest.fn();
|
||||
const items = [
|
||||
{
|
||||
id: '1',
|
||||
humanData: { label: 'label1', position: 1 },
|
||||
onDrop,
|
||||
dropType: 'reorder' as DropType,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
humanData: { label: 'label2', position: 2 },
|
||||
onDrop,
|
||||
dropType: 'reorder' as DropType,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
humanData: { label: 'label3', position: 3 },
|
||||
onDrop,
|
||||
dropType: 'reorder' as DropType,
|
||||
},
|
||||
];
|
||||
const mountComponent = (
|
||||
dragContext: Partial<DragContextState> | undefined,
|
||||
onDrop: DropHandler = jest.fn()
|
||||
onDropHandler?: () => void
|
||||
) => {
|
||||
let dragging = dragContext?.dragging;
|
||||
let keyboardMode = !!dragContext?.keyboardMode;
|
||||
let activeDropTarget = dragContext?.activeDropTarget;
|
||||
|
||||
const setA11yMessage = jest.fn();
|
||||
const registerDropTarget = jest.fn();
|
||||
const baseContext = {
|
||||
dragging,
|
||||
setDragging: (val?: DragDropIdentifier) => {
|
||||
|
@ -280,70 +435,51 @@ describe('DragDrop', () => {
|
|||
keyboardMode = mode;
|
||||
}),
|
||||
setActiveDropTarget: (target?: DragDropIdentifier) => {
|
||||
activeDropTarget = { activeDropTarget: target } as ActiveDropTarget;
|
||||
activeDropTarget = { activeDropTarget: target } as DropTargets;
|
||||
},
|
||||
activeDropTarget,
|
||||
setA11yMessage: jest.fn(),
|
||||
setA11yMessage,
|
||||
registerDropTarget,
|
||||
};
|
||||
|
||||
const dragDropSharedProps = {
|
||||
draggable: true,
|
||||
dragType: 'move' as 'copy' | 'move',
|
||||
dropType: 'reorder' as DropType,
|
||||
reorderableGroup: items.map(({ id }) => ({ id })),
|
||||
onDrop: onDropHandler || onDrop,
|
||||
};
|
||||
|
||||
return mount(
|
||||
<ChildDragDropProvider {...baseContext} {...dragContext}>
|
||||
<ReorderProvider id="groupId">
|
||||
<DragDrop
|
||||
label="1"
|
||||
draggable
|
||||
droppable={false}
|
||||
dragType="reorder"
|
||||
dropType="reorder"
|
||||
reorderableGroup={[{ id: '1' }, { id: '2' }, { id: '3' }]}
|
||||
value={{ id: '1' }}
|
||||
onDrop={onDrop}
|
||||
{...dragDropSharedProps}
|
||||
value={items[0]}
|
||||
dropType={undefined}
|
||||
order={[2, 0, 0]}
|
||||
>
|
||||
<span>1</span>
|
||||
</DragDrop>
|
||||
<DragDrop
|
||||
label="2"
|
||||
draggable
|
||||
droppable
|
||||
dragType="reorder"
|
||||
dropType="reorder"
|
||||
reorderableGroup={[{ id: '1' }, { id: '2' }, { id: '3' }]}
|
||||
value={{
|
||||
id: '2',
|
||||
}}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
<DragDrop {...dragDropSharedProps} value={items[1]} order={[2, 0, 1]}>
|
||||
<span>2</span>
|
||||
</DragDrop>
|
||||
<DragDrop
|
||||
label="3"
|
||||
draggable
|
||||
droppable
|
||||
dragType="reorder"
|
||||
dropType="reorder"
|
||||
reorderableGroup={[{ id: '1' }, { id: '2' }, { id: '3' }]}
|
||||
value={{
|
||||
id: '3',
|
||||
}}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
<DragDrop {...dragDropSharedProps} value={items[2]} order={[2, 0, 2]}>
|
||||
<span>3</span>
|
||||
</DragDrop>
|
||||
</ReorderProvider>
|
||||
</ChildDragDropProvider>
|
||||
);
|
||||
};
|
||||
test(`Inactive reorderable group renders properly`, () => {
|
||||
const component = mountComponent(undefined, jest.fn());
|
||||
expect(component.find('.lnsDragDrop-reorderable')).toHaveLength(3);
|
||||
test(`Inactive group renders properly`, () => {
|
||||
const component = mountComponent(undefined);
|
||||
expect(component.find('[data-test-subj="lnsDragDrop"]')).toHaveLength(3);
|
||||
});
|
||||
|
||||
test(`Reorderable group with lifted element renders properly`, () => {
|
||||
const setDragging = jest.fn();
|
||||
const setA11yMessage = jest.fn();
|
||||
const component = mountComponent(
|
||||
{ dragging: { id: '1' }, setA11yMessage, setDragging },
|
||||
jest.fn()
|
||||
);
|
||||
const setDragging = jest.fn();
|
||||
const component = mountComponent({ dragging: items[0], setDragging, setA11yMessage });
|
||||
act(() => {
|
||||
component
|
||||
.find('[data-test-subj="lnsDragDrop"]')
|
||||
|
@ -352,8 +488,8 @@ describe('DragDrop', () => {
|
|||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(setDragging).toBeCalledWith({ id: '1' });
|
||||
expect(setA11yMessage).toBeCalledWith('You have lifted an item 1 in position 1');
|
||||
expect(setDragging).toBeCalledWith(items[0]);
|
||||
expect(setA11yMessage).toBeCalledWith('Lifted label1');
|
||||
expect(
|
||||
component
|
||||
.find('[data-test-subj="lnsDragDrop-reorderableGroup"]')
|
||||
|
@ -362,7 +498,7 @@ describe('DragDrop', () => {
|
|||
});
|
||||
|
||||
test(`Reordered elements get extra styles to show the reorder effect when dragging`, () => {
|
||||
const component = mountComponent({ dragging: { id: '1' } }, jest.fn());
|
||||
const component = mountComponent({ dragging: items[0] });
|
||||
|
||||
act(() => {
|
||||
component
|
||||
|
@ -403,16 +539,13 @@ describe('DragDrop', () => {
|
|||
});
|
||||
|
||||
test(`Dropping an item runs onDrop function`, () => {
|
||||
const setDragging = jest.fn();
|
||||
const setA11yMessage = jest.fn();
|
||||
const preventDefault = jest.fn();
|
||||
const stopPropagation = jest.fn();
|
||||
const onDrop = jest.fn();
|
||||
|
||||
const component = mountComponent(
|
||||
{ dragging: { id: '1' }, setA11yMessage, setDragging },
|
||||
onDrop
|
||||
);
|
||||
const setA11yMessage = jest.fn();
|
||||
const setDragging = jest.fn();
|
||||
|
||||
const component = mountComponent({ dragging: items[0], setDragging, setA11yMessage });
|
||||
|
||||
component
|
||||
.find('[data-test-subj="lnsDragDrop-reorderableDropLayer"]')
|
||||
|
@ -421,23 +554,58 @@ describe('DragDrop', () => {
|
|||
jest.runAllTimers();
|
||||
|
||||
expect(setA11yMessage).toBeCalledWith(
|
||||
'You have dropped the item. You have moved the item from position 1 to positon 3'
|
||||
'You have dropped the item label1. You have moved the item from position 1 to positon 3'
|
||||
);
|
||||
expect(preventDefault).toBeCalled();
|
||||
expect(stopPropagation).toBeCalled();
|
||||
expect(onDrop).toBeCalledWith({ id: '1' }, { id: '3' });
|
||||
expect(onDrop).toBeCalledWith(items[0], 'reorder');
|
||||
});
|
||||
|
||||
test(`Keyboard navigation: user can drop element to an activeDropTarget`, () => {
|
||||
const onDrop = jest.fn();
|
||||
const component = mountComponent(
|
||||
{
|
||||
dragging: { id: '1' },
|
||||
activeDropTarget: { activeDropTarget: { id: '3' } } as ActiveDropTarget,
|
||||
keyboardMode: true,
|
||||
test(`Keyboard Navigation: User cannot move an element outside of the group`, () => {
|
||||
const setA11yMessage = jest.fn();
|
||||
const setActiveDropTarget = jest.fn();
|
||||
const component = mountComponent({
|
||||
dragging: items[0],
|
||||
keyboardMode: true,
|
||||
activeDropTarget: {
|
||||
activeDropTarget: undefined,
|
||||
dropTargetsByOrder: {
|
||||
'2,0,0': undefined,
|
||||
'2,0,1': { ...items[1], onDrop, dropType: 'reorder' },
|
||||
'2,0,2': { ...items[2], onDrop, dropType: 'reorder' },
|
||||
},
|
||||
},
|
||||
onDrop
|
||||
setActiveDropTarget,
|
||||
setA11yMessage,
|
||||
});
|
||||
const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]');
|
||||
|
||||
keyboardHandler.simulate('keydown', { key: 'Space' });
|
||||
keyboardHandler.simulate('keydown', { key: 'ArrowUp' });
|
||||
expect(setActiveDropTarget).not.toHaveBeenCalled();
|
||||
|
||||
keyboardHandler.simulate('keydown', { key: 'Space' });
|
||||
keyboardHandler.simulate('keydown', { key: 'ArrowDown' });
|
||||
|
||||
expect(setActiveDropTarget).toBeCalledWith(items[1]);
|
||||
expect(setA11yMessage).toBeCalledWith(
|
||||
'You have moved the item label1 from position 1 to position 2'
|
||||
);
|
||||
});
|
||||
test(`Keyboard navigation: user can drop element to an activeDropTarget`, () => {
|
||||
const component = mountComponent({
|
||||
dragging: items[0],
|
||||
activeDropTarget: {
|
||||
activeDropTarget: { ...items[2], dropType: 'reorder', onDrop },
|
||||
dropTargetsByOrder: {
|
||||
'2,0,0': { ...items[0], onDrop, dropType: 'reorder' },
|
||||
'2,0,1': { ...items[1], onDrop, dropType: 'reorder' },
|
||||
'2,0,2': { ...items[2], onDrop, dropType: 'reorder' },
|
||||
},
|
||||
},
|
||||
|
||||
keyboardMode: true,
|
||||
});
|
||||
const keyboardHandler = component
|
||||
.find('[data-test-subj="lnsDragDrop-keyboardHandler"]')
|
||||
.simulate('focus');
|
||||
|
@ -447,15 +615,43 @@ describe('DragDrop', () => {
|
|||
keyboardHandler.simulate('keydown', { key: 'ArrowDown' });
|
||||
keyboardHandler.simulate('keydown', { key: 'Enter' });
|
||||
});
|
||||
expect(onDrop).toBeCalledWith({ id: '1' }, { id: '3' });
|
||||
expect(onDrop).toBeCalledWith(items[0], 'reorder');
|
||||
});
|
||||
|
||||
test(`Keyboard Navigation: Doesn't call onDrop when movement is cancelled`, () => {
|
||||
const setA11yMessage = jest.fn();
|
||||
const onDropHandler = jest.fn();
|
||||
const component = mountComponent({ dragging: items[0], setA11yMessage }, onDropHandler);
|
||||
const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]');
|
||||
keyboardHandler.simulate('keydown', { key: 'Space' });
|
||||
keyboardHandler.simulate('keydown', { key: 'Escape' });
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(onDropHandler).not.toHaveBeenCalled();
|
||||
expect(setA11yMessage).toBeCalledWith('Movement cancelled');
|
||||
keyboardHandler.simulate('keydown', { key: 'Space' });
|
||||
keyboardHandler.simulate('keydown', { key: 'ArrowDown' });
|
||||
keyboardHandler.simulate('blur');
|
||||
|
||||
expect(onDropHandler).not.toHaveBeenCalled();
|
||||
expect(setA11yMessage).toBeCalledWith('Movement cancelled');
|
||||
});
|
||||
|
||||
test(`Keyboard Navigation: Reordered elements get extra styles to show the reorder effect`, () => {
|
||||
const setA11yMessage = jest.fn();
|
||||
const component = mountComponent(
|
||||
{ dragging: { id: '1' }, keyboardMode: true, setA11yMessage },
|
||||
jest.fn()
|
||||
);
|
||||
const component = mountComponent({
|
||||
dragging: items[0],
|
||||
keyboardMode: true,
|
||||
activeDropTarget: {
|
||||
activeDropTarget: undefined,
|
||||
dropTargetsByOrder: {
|
||||
'2,0,0': undefined,
|
||||
'2,0,1': { ...items[1], onDrop, dropType: 'reorder' },
|
||||
'2,0,2': { ...items[2], onDrop, dropType: 'reorder' },
|
||||
},
|
||||
},
|
||||
setA11yMessage,
|
||||
});
|
||||
|
||||
const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]');
|
||||
keyboardHandler.simulate('keydown', { key: 'Space' });
|
||||
|
@ -475,7 +671,7 @@ describe('DragDrop', () => {
|
|||
component.find('[data-test-subj="lnsDragDrop-translatableDrop"]').at(1).prop('style')
|
||||
).toEqual(undefined);
|
||||
expect(setA11yMessage).toBeCalledWith(
|
||||
'You have moved the item 1 from position 1 to position 2'
|
||||
'You have moved the item label1 from position 1 to position 2'
|
||||
);
|
||||
|
||||
component
|
||||
|
@ -490,63 +686,45 @@ describe('DragDrop', () => {
|
|||
).toEqual(undefined);
|
||||
});
|
||||
|
||||
test(`Keyboard Navigation: User cannot move an element outside of the group`, () => {
|
||||
const onDrop = jest.fn();
|
||||
const setActiveDropTarget = jest.fn();
|
||||
const setA11yMessage = jest.fn();
|
||||
const component = mountComponent(
|
||||
{ dragging: { id: '1' }, keyboardMode: true, setActiveDropTarget, setA11yMessage },
|
||||
onDrop
|
||||
);
|
||||
const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]');
|
||||
|
||||
keyboardHandler.simulate('keydown', { key: 'Space' });
|
||||
keyboardHandler.simulate('keydown', { key: 'ArrowUp' });
|
||||
expect(setActiveDropTarget).not.toHaveBeenCalled();
|
||||
|
||||
keyboardHandler.simulate('keydown', { key: 'Space' });
|
||||
keyboardHandler.simulate('keydown', { key: 'ArrowDown' });
|
||||
|
||||
expect(setActiveDropTarget).toBeCalledWith({ id: '2' });
|
||||
expect(setA11yMessage).toBeCalledWith(
|
||||
'You have moved the item 1 from position 1 to position 2'
|
||||
);
|
||||
});
|
||||
|
||||
test(`Keyboard Navigation: User cannot drop element to itself`, () => {
|
||||
const setActiveDropTarget = jest.fn();
|
||||
const setA11yMessage = jest.fn();
|
||||
const setActiveDropTarget = jest.fn();
|
||||
const component = mount(
|
||||
<ChildDragDropProvider
|
||||
{...defaultContext}
|
||||
keyboardMode={true}
|
||||
activeDropTarget={{
|
||||
activeDropTarget: { id: '2' },
|
||||
activeDropTarget: {
|
||||
...items[1],
|
||||
onDrop,
|
||||
dropType: 'reorder',
|
||||
},
|
||||
dropTargetsByOrder: {
|
||||
'2,0,1,0': undefined,
|
||||
'2,0,1,1': { ...items[1], onDrop, dropType: 'reorder' },
|
||||
},
|
||||
}}
|
||||
dragging={{ id: '1' }}
|
||||
dragging={items[0]}
|
||||
setActiveDropTarget={setActiveDropTarget}
|
||||
setA11yMessage={setA11yMessage}
|
||||
>
|
||||
<ReorderProvider id="groupId">
|
||||
<DragDrop
|
||||
label="1"
|
||||
draggable
|
||||
droppable={false}
|
||||
dragType="reorder"
|
||||
dropType="reorder"
|
||||
reorderableGroup={[{ id: '1' }, { id: '2' }]}
|
||||
value={{ id: '1' }}
|
||||
dragType="move"
|
||||
reorderableGroup={[items[0], items[1]]}
|
||||
value={items[0]}
|
||||
order={[2, 0, 1, 0]}
|
||||
>
|
||||
<span>1</span>
|
||||
</DragDrop>
|
||||
<DragDrop
|
||||
label="2"
|
||||
draggable
|
||||
droppable
|
||||
dragType="reorder"
|
||||
dragType="move"
|
||||
dropType="reorder"
|
||||
reorderableGroup={[{ id: '1' }, { id: '2' }]}
|
||||
value={{ id: '2' }}
|
||||
reorderableGroup={[items[0], items[1]]}
|
||||
value={items[1]}
|
||||
order={[2, 0, 1, 1]}
|
||||
>
|
||||
<span>2</span>
|
||||
</DragDrop>
|
||||
|
@ -557,33 +735,8 @@ describe('DragDrop', () => {
|
|||
|
||||
keyboardHandler.simulate('keydown', { key: 'Space' });
|
||||
keyboardHandler.simulate('keydown', { key: 'ArrowUp' });
|
||||
expect(setActiveDropTarget).toBeCalledWith({ id: '1' });
|
||||
expect(setA11yMessage).toBeCalledWith('You have moved back the item 1 to position 1');
|
||||
});
|
||||
|
||||
test(`Keyboard Navigation: Doesn't call onDrop when movement is cancelled`, () => {
|
||||
const setA11yMessage = jest.fn();
|
||||
const onDrop = jest.fn();
|
||||
|
||||
const component = mountComponent({ dragging: { id: '1' }, setA11yMessage }, onDrop);
|
||||
const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]');
|
||||
keyboardHandler.simulate('keydown', { key: 'Space' });
|
||||
keyboardHandler.simulate('keydown', { key: 'Escape' });
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(onDrop).not.toHaveBeenCalled();
|
||||
expect(setA11yMessage).toBeCalledWith(
|
||||
'Movement cancelled. The item has returned to its starting position 1'
|
||||
);
|
||||
keyboardHandler.simulate('keydown', { key: 'Space' });
|
||||
keyboardHandler.simulate('keydown', { key: 'ArrowDown' });
|
||||
keyboardHandler.simulate('blur');
|
||||
|
||||
expect(onDrop).not.toHaveBeenCalled();
|
||||
expect(setA11yMessage).toBeCalledWith(
|
||||
'Movement cancelled. The item has returned to its starting position 1'
|
||||
);
|
||||
expect(setActiveDropTarget).toBeCalledWith(undefined);
|
||||
expect(setA11yMessage).toBeCalledWith('You have moved the item label1 back to position 1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,23 +9,23 @@ import './drag_drop.scss';
|
|||
import React, { useContext, useEffect, memo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { keys, EuiScreenReaderOnly } from '@elastic/eui';
|
||||
import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect';
|
||||
import {
|
||||
DragDropIdentifier,
|
||||
DropIdentifier,
|
||||
DragContext,
|
||||
DragContextState,
|
||||
nextValidDropTarget,
|
||||
ReorderContext,
|
||||
ReorderState,
|
||||
reorderAnnouncements,
|
||||
DropHandler,
|
||||
} from './providers';
|
||||
import { announce } from './announcements';
|
||||
import { trackUiEvent } from '../lens_ui_telemetry';
|
||||
import { DropType } from '../types';
|
||||
|
||||
export type DroppableEvent = React.DragEvent<HTMLElement>;
|
||||
|
||||
/**
|
||||
* A function that handles a drop event.
|
||||
*/
|
||||
export type DropHandler = (dropped: DragDropIdentifier, dropTarget: DragDropIdentifier) => void;
|
||||
|
||||
/**
|
||||
* The base props to the DragDrop component.
|
||||
*/
|
||||
|
@ -34,10 +34,6 @@ interface BaseProps {
|
|||
* The CSS class(es) for the root element.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* The label for accessibility
|
||||
*/
|
||||
label?: string;
|
||||
|
||||
/**
|
||||
* The event handler that fires when an item
|
||||
|
@ -62,16 +58,15 @@ interface BaseProps {
|
|||
* Indicates whether or not this component is draggable.
|
||||
*/
|
||||
draggable?: boolean;
|
||||
/**
|
||||
* Indicates whether or not the currently dragged item
|
||||
* can be dropped onto this component.
|
||||
*/
|
||||
droppable?: boolean;
|
||||
|
||||
/**
|
||||
* Additional class names to apply when another element is over the drop target
|
||||
*/
|
||||
getAdditionalClassesOnEnter?: () => string;
|
||||
getAdditionalClassesOnEnter?: (dropType?: DropType) => string | undefined;
|
||||
/**
|
||||
* Additional class names to apply when another element is droppable for a currently dragged item
|
||||
*/
|
||||
getAdditionalClassesOnDroppable?: (dropType?: DropType) => string | undefined;
|
||||
|
||||
/**
|
||||
* The optional test subject associated with this DOM element.
|
||||
|
@ -81,35 +76,29 @@ interface BaseProps {
|
|||
/**
|
||||
* items belonging to the same group that can be reordered
|
||||
*/
|
||||
reorderableGroup?: DragDropIdentifier[];
|
||||
reorderableGroup?: Array<{ id: string }>;
|
||||
|
||||
/**
|
||||
* Indicates to the user whether the currently dragged item
|
||||
* will be moved or copied
|
||||
*/
|
||||
dragType?: 'copy' | 'move' | 'reorder';
|
||||
dragType?: 'copy' | 'move';
|
||||
|
||||
/**
|
||||
* Indicates to the user whether the drop action will
|
||||
* replace something that is existing or add a new one
|
||||
* Indicates the type of a drop - when undefined, the currently dragged item
|
||||
* cannot be dropped onto this component.
|
||||
*/
|
||||
dropType?: 'add' | 'replace' | 'reorder';
|
||||
|
||||
dropType?: DropType;
|
||||
/**
|
||||
* temporary flag to exclude the draggable elements that don't have keyboard nav yet. To be removed along with the feature development
|
||||
* Order for keyboard dragging. This takes an array of numbers which will be used to order hierarchically
|
||||
*/
|
||||
noKeyboardSupportYet?: boolean;
|
||||
order: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The props for a draggable instance of that component.
|
||||
*/
|
||||
interface DragInnerProps extends BaseProps {
|
||||
/**
|
||||
* The label, which should be attached to the drag event, and which will e.g.
|
||||
* be used if the element will be dropped into a text field.
|
||||
*/
|
||||
label?: string;
|
||||
isDragging: boolean;
|
||||
keyboardMode: boolean;
|
||||
setKeyboardMode: DragContextState['setKeyboardMode'];
|
||||
|
@ -124,6 +113,7 @@ interface DragInnerProps extends BaseProps {
|
|||
) => void;
|
||||
onDragEnd?: () => void;
|
||||
extraKeyboardHandler?: (e: React.KeyboardEvent<HTMLButtonElement>) => void;
|
||||
ariaDescribedBy?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -131,23 +121,16 @@ interface DragInnerProps extends BaseProps {
|
|||
*/
|
||||
interface DropInnerProps extends BaseProps, DragContextState {
|
||||
isDragging: boolean;
|
||||
|
||||
isNotDroppable: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A draggable / droppable item. Items can be both draggable and droppable at
|
||||
* the same time.
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
|
||||
const lnsLayerPanelDimensionMargin = 8;
|
||||
|
||||
export const DragDrop = (props: BaseProps) => {
|
||||
const {
|
||||
dragging,
|
||||
setDragging,
|
||||
registerDropTarget,
|
||||
keyboardMode,
|
||||
setKeyboardMode,
|
||||
activeDropTarget,
|
||||
|
@ -155,8 +138,7 @@ export const DragDrop = (props: BaseProps) => {
|
|||
setA11yMessage,
|
||||
} = useContext(DragContext);
|
||||
|
||||
const { value, draggable, droppable, reorderableGroup } = props;
|
||||
|
||||
const { value, draggable, dropType, reorderableGroup } = props;
|
||||
const isDragging = !!(draggable && value.id === dragging?.id);
|
||||
|
||||
const dragProps = {
|
||||
|
@ -178,16 +160,17 @@ export const DragDrop = (props: BaseProps) => {
|
|||
setDragging,
|
||||
activeDropTarget,
|
||||
setActiveDropTarget,
|
||||
registerDropTarget,
|
||||
isDragging,
|
||||
setA11yMessage,
|
||||
isNotDroppable:
|
||||
// If the configuration has provided a droppable flag, but this particular item is not
|
||||
// droppable, then it should be less prominent. Ignores items that are both
|
||||
// draggable and drop targets
|
||||
!!(droppable === false && dragging && value.id !== dragging.id),
|
||||
!!(!dropType && dragging && value.id !== dragging.id),
|
||||
};
|
||||
|
||||
if (draggable && !droppable) {
|
||||
if (draggable && !dropType) {
|
||||
if (reorderableGroup && reorderableGroup.length > 1) {
|
||||
return (
|
||||
<ReorderableDrag
|
||||
|
@ -204,14 +187,14 @@ export const DragDrop = (props: BaseProps) => {
|
|||
if (
|
||||
reorderableGroup &&
|
||||
reorderableGroup.length > 1 &&
|
||||
reorderableGroup?.some((i) => i.id === value.id)
|
||||
reorderableGroup?.some((i) => i.id === dragging?.id)
|
||||
) {
|
||||
return <ReorderableDrop reorderableGroup={reorderableGroup} {...dropProps} />;
|
||||
return <ReorderableDrop {...dropProps} reorderableGroup={reorderableGroup} />;
|
||||
}
|
||||
return <DropInner {...dropProps} />;
|
||||
};
|
||||
|
||||
const DragInner = memo(function DragDropInner({
|
||||
const DragInner = memo(function DragInner({
|
||||
dataTestSubj,
|
||||
className,
|
||||
value,
|
||||
|
@ -219,16 +202,16 @@ const DragInner = memo(function DragDropInner({
|
|||
setDragging,
|
||||
setKeyboardMode,
|
||||
setActiveDropTarget,
|
||||
label = '',
|
||||
order,
|
||||
keyboardMode,
|
||||
isDragging,
|
||||
activeDropTarget,
|
||||
onDrop,
|
||||
dragType,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
extraKeyboardHandler,
|
||||
noKeyboardSupportYet,
|
||||
ariaDescribedBy,
|
||||
setA11yMessage,
|
||||
}: DragInnerProps) {
|
||||
const dragStart = (e?: DroppableEvent | React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
// Setting stopPropgagation causes Chrome failures, so
|
||||
|
@ -241,7 +224,7 @@ const DragInner = memo(function DragDropInner({
|
|||
// 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', label);
|
||||
e.dataTransfer.setData('text', value.humanData.label);
|
||||
}
|
||||
|
||||
// Chrome causes issues if you try to render from within a
|
||||
|
@ -250,6 +233,7 @@ const DragInner = memo(function DragDropInner({
|
|||
const currentTarget = e?.currentTarget;
|
||||
setTimeout(() => {
|
||||
setDragging(value);
|
||||
setA11yMessage(announce.lifted(value.humanData));
|
||||
if (onDragStart) {
|
||||
onDragStart(currentTarget);
|
||||
}
|
||||
|
@ -261,53 +245,78 @@ const DragInner = memo(function DragDropInner({
|
|||
setDragging(undefined);
|
||||
setActiveDropTarget(undefined);
|
||||
setKeyboardMode(false);
|
||||
setA11yMessage(announce.cancelled());
|
||||
if (onDragEnd) {
|
||||
onDragEnd();
|
||||
}
|
||||
};
|
||||
|
||||
const dropToActiveDropTarget = () => {
|
||||
if (isDragging && activeDropTarget?.activeDropTarget) {
|
||||
trackUiEvent('drop_total');
|
||||
if (onDrop) {
|
||||
onDrop(value, activeDropTarget.activeDropTarget);
|
||||
}
|
||||
const { dropType, humanData, onDrop: onTargetDrop } = activeDropTarget.activeDropTarget;
|
||||
setTimeout(() => setA11yMessage(announce.dropped(value.humanData, humanData, dropType)));
|
||||
onTargetDrop(value, dropType);
|
||||
}
|
||||
};
|
||||
|
||||
const setNextTarget = (reversed = false) => {
|
||||
if (!order) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextTarget = nextValidDropTarget(
|
||||
activeDropTarget,
|
||||
[order.join(',')],
|
||||
(el) => el?.dropType !== 'reorder',
|
||||
reversed
|
||||
);
|
||||
|
||||
setActiveDropTarget(nextTarget);
|
||||
setA11yMessage(
|
||||
nextTarget
|
||||
? announce.selectedTarget(value.humanData, nextTarget?.humanData, nextTarget?.dropType)
|
||||
: announce.noTarget()
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div className={className}>
|
||||
{!noKeyboardSupportYet && (
|
||||
<EuiScreenReaderOnly showOnFocus>
|
||||
<button
|
||||
aria-label={label}
|
||||
aria-describedby={`lnsDragDrop-keyboardInstructions`}
|
||||
className="lnsDragDrop__keyboardHandler"
|
||||
data-test-subj="lnsDragDrop-keyboardHandler"
|
||||
onBlur={() => {
|
||||
<div className={className} data-test-subj={`lnsDragDrop_draggable-${value.humanData.label}`}>
|
||||
<EuiScreenReaderOnly showOnFocus>
|
||||
<button
|
||||
aria-label={value.humanData.label}
|
||||
aria-describedby={ariaDescribedBy || `lnsDragDrop-keyboardInstructions`}
|
||||
className="lnsDragDrop__keyboardHandler"
|
||||
data-test-subj="lnsDragDrop-keyboardHandler"
|
||||
onBlur={() => {
|
||||
if (isDragging) {
|
||||
dragEnd();
|
||||
}}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (e.key === keys.ENTER || e.key === keys.SPACE) {
|
||||
if (activeDropTarget) {
|
||||
dropToActiveDropTarget();
|
||||
}
|
||||
if (isDragging) {
|
||||
dragEnd();
|
||||
} else {
|
||||
dragStart(e);
|
||||
setKeyboardMode(true);
|
||||
}
|
||||
} else if (e.key === keys.ESCAPE) {
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
const { key } = e;
|
||||
if (key === keys.ENTER || key === keys.SPACE) {
|
||||
if (activeDropTarget) {
|
||||
dropToActiveDropTarget();
|
||||
}
|
||||
if (isDragging) {
|
||||
dragEnd();
|
||||
} else {
|
||||
dragStart(e);
|
||||
setKeyboardMode(true);
|
||||
}
|
||||
} else if (key === keys.ESCAPE) {
|
||||
if (isDragging) {
|
||||
dragEnd();
|
||||
}
|
||||
if (extraKeyboardHandler) {
|
||||
extraKeyboardHandler(e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiScreenReaderOnly>
|
||||
)}
|
||||
}
|
||||
if (extraKeyboardHandler) {
|
||||
extraKeyboardHandler(e);
|
||||
}
|
||||
if (keyboardMode && (keys.ARROW_LEFT === key || keys.ARROW_RIGHT === key)) {
|
||||
setNextTarget(!!(keys.ARROW_LEFT === key));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiScreenReaderOnly>
|
||||
|
||||
{React.cloneElement(children, {
|
||||
'data-test-subj': dataTestSubj || 'lnsDragDrop',
|
||||
|
@ -329,26 +338,41 @@ const DropInner = memo(function DropInner(props: DropInnerProps) {
|
|||
onDrop,
|
||||
value,
|
||||
children,
|
||||
droppable,
|
||||
draggable,
|
||||
dragging,
|
||||
setDragging,
|
||||
isDragging,
|
||||
isNotDroppable,
|
||||
dragType = 'copy',
|
||||
dropType = 'add',
|
||||
dropType,
|
||||
keyboardMode,
|
||||
setKeyboardMode,
|
||||
activeDropTarget,
|
||||
registerDropTarget,
|
||||
setActiveDropTarget,
|
||||
getAdditionalClassesOnEnter,
|
||||
getAdditionalClassesOnDroppable,
|
||||
setKeyboardMode,
|
||||
setDragging,
|
||||
order,
|
||||
setA11yMessage,
|
||||
} = props;
|
||||
|
||||
useShallowCompareEffect(() => {
|
||||
if (dropType && value && onDrop) {
|
||||
registerDropTarget(order, { ...value, onDrop, dropType });
|
||||
return () => {
|
||||
registerDropTarget(order, undefined);
|
||||
};
|
||||
}
|
||||
}, [order, value, registerDropTarget, dropType]);
|
||||
|
||||
const activeDropTargetMatches =
|
||||
activeDropTarget?.activeDropTarget && activeDropTarget.activeDropTarget.id === value.id;
|
||||
|
||||
const isMoveDragging = isDragging && dragType === 'move';
|
||||
|
||||
const classesOnEnter = getAdditionalClassesOnEnter?.(dropType);
|
||||
const classesOnDroppable = getAdditionalClassesOnDroppable?.(dropType);
|
||||
|
||||
const classes = classNames(
|
||||
'lnsDragDrop',
|
||||
{
|
||||
|
@ -356,27 +380,25 @@ const DropInner = memo(function DropInner(props: DropInnerProps) {
|
|||
'lnsDragDrop-isDragging': isDragging,
|
||||
'lnsDragDrop-isHidden': isMoveDragging && !keyboardMode,
|
||||
'lnsDragDrop-isDroppable': !draggable,
|
||||
'lnsDragDrop-isDropTarget': droppable && dragType !== 'reorder',
|
||||
'lnsDragDrop-isDropTarget': dropType && dropType !== 'reorder',
|
||||
'lnsDragDrop-isActiveDropTarget':
|
||||
droppable && activeDropTargetMatches && dragType !== 'reorder',
|
||||
dropType && activeDropTargetMatches && dropType !== 'reorder',
|
||||
'lnsDragDrop-isNotDroppable': !isMoveDragging && isNotDroppable,
|
||||
'lnsDragDrop-isReplacing': droppable && activeDropTargetMatches && dropType === 'replace',
|
||||
},
|
||||
getAdditionalClassesOnEnter && {
|
||||
[getAdditionalClassesOnEnter()]: activeDropTargetMatches,
|
||||
}
|
||||
classesOnEnter && { [classesOnEnter]: activeDropTargetMatches },
|
||||
classesOnDroppable && { [classesOnDroppable]: dropType }
|
||||
);
|
||||
|
||||
const dragOver = (e: DroppableEvent) => {
|
||||
if (!droppable) {
|
||||
if (!dropType) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
// An optimization to prevent a bunch of React churn.
|
||||
// todo: replace with custom function ?
|
||||
if (!activeDropTargetMatches) {
|
||||
setActiveDropTarget(value);
|
||||
if (!activeDropTargetMatches && dragging && onDrop) {
|
||||
setActiveDropTarget({ ...value, dropType, onDrop });
|
||||
setA11yMessage(announce.selectedTarget(dragging.humanData, value.humanData, dropType));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -388,12 +410,15 @@ const DropInner = memo(function DropInner(props: DropInnerProps) {
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (onDrop && droppable && dragging) {
|
||||
if (onDrop && dropType && dragging) {
|
||||
trackUiEvent('drop_total');
|
||||
onDrop(dragging, value);
|
||||
onDrop(dragging, dropType);
|
||||
setTimeout(() =>
|
||||
setA11yMessage(announce.dropped(dragging.humanData, value.humanData, dropType))
|
||||
);
|
||||
}
|
||||
setActiveDropTarget(undefined);
|
||||
setDragging(undefined);
|
||||
setActiveDropTarget(undefined);
|
||||
setKeyboardMode(false);
|
||||
};
|
||||
return (
|
||||
|
@ -411,7 +436,7 @@ const DropInner = memo(function DropInner(props: DropInnerProps) {
|
|||
});
|
||||
|
||||
const ReorderableDrag = memo(function ReorderableDrag(
|
||||
props: DragInnerProps & { reorderableGroup: DragDropIdentifier[]; dragging?: DragDropIdentifier }
|
||||
props: DragInnerProps & { reorderableGroup: Array<{ id: string }>; dragging?: DragDropIdentifier }
|
||||
) {
|
||||
const {
|
||||
reorderState: { isReorderOn, reorderedItems, direction },
|
||||
|
@ -421,17 +446,13 @@ const ReorderableDrag = memo(function ReorderableDrag(
|
|||
const {
|
||||
value,
|
||||
setActiveDropTarget,
|
||||
label = '',
|
||||
keyboardMode,
|
||||
isDragging,
|
||||
activeDropTarget,
|
||||
reorderableGroup,
|
||||
onDrop,
|
||||
setA11yMessage,
|
||||
} = props;
|
||||
|
||||
const currentIndex = reorderableGroup.findIndex((i) => i.id === value.id);
|
||||
|
||||
const isFocusInGroup = keyboardMode
|
||||
? isDragging &&
|
||||
(!activeDropTarget?.activeDropTarget ||
|
||||
|
@ -457,25 +478,10 @@ const ReorderableDrag = memo(function ReorderableDrag(
|
|||
draggingHeight: height,
|
||||
}));
|
||||
}
|
||||
|
||||
setA11yMessage(reorderAnnouncements.lifted(label, currentIndex + 1));
|
||||
};
|
||||
|
||||
const onReorderableDragEnd = () => {
|
||||
resetReorderState();
|
||||
setA11yMessage(reorderAnnouncements.cancelled(currentIndex + 1));
|
||||
};
|
||||
|
||||
const onReorderableDrop = (dragging: DragDropIdentifier, target: DragDropIdentifier) => {
|
||||
if (onDrop) {
|
||||
onDrop(dragging, target);
|
||||
const targetIndex = reorderableGroup.findIndex(
|
||||
(i) => i.id === activeDropTarget?.activeDropTarget?.id
|
||||
);
|
||||
|
||||
resetReorderState();
|
||||
setA11yMessage(reorderAnnouncements.dropped(targetIndex + 1, currentIndex + 1));
|
||||
}
|
||||
};
|
||||
|
||||
const resetReorderState = () =>
|
||||
|
@ -495,42 +501,50 @@ const ReorderableDrag = memo(function ReorderableDrag(
|
|||
);
|
||||
if (index !== -1) activeDropTargetIndex = index;
|
||||
}
|
||||
if (keys.ARROW_DOWN === e.key) {
|
||||
if (e.key === keys.ARROW_LEFT || e.key === keys.ARROW_RIGHT) {
|
||||
resetReorderState();
|
||||
setActiveDropTarget(undefined);
|
||||
} else if (keys.ARROW_DOWN === e.key) {
|
||||
if (activeDropTargetIndex < reorderableGroup.length - 1) {
|
||||
setA11yMessage(
|
||||
reorderAnnouncements.moved(label, activeDropTargetIndex + 2, currentIndex + 1)
|
||||
const nextTarget = nextValidDropTarget(
|
||||
activeDropTarget,
|
||||
[props.order.join(',')],
|
||||
(el) => el?.dropType === 'reorder'
|
||||
);
|
||||
onReorderableDragOver(reorderableGroup[activeDropTargetIndex + 1]);
|
||||
onReorderableDragOver(nextTarget);
|
||||
}
|
||||
} else if (keys.ARROW_UP === e.key) {
|
||||
if (activeDropTargetIndex > 0) {
|
||||
setA11yMessage(
|
||||
reorderAnnouncements.moved(label, activeDropTargetIndex, currentIndex + 1)
|
||||
const nextTarget = nextValidDropTarget(
|
||||
activeDropTarget,
|
||||
[props.order.join(',')],
|
||||
(el) => el?.dropType === 'reorder',
|
||||
true
|
||||
);
|
||||
|
||||
onReorderableDragOver(reorderableGroup[activeDropTargetIndex - 1]);
|
||||
onReorderableDragOver(nextTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onReorderableDragOver = (target: DragDropIdentifier) => {
|
||||
let droppingIndex = currentIndex;
|
||||
if (keyboardMode && 'id' in target) {
|
||||
setActiveDropTarget(target);
|
||||
droppingIndex = reorderableGroup.findIndex((i) => i.id === target.id);
|
||||
}
|
||||
const draggingIndex = reorderableGroup.findIndex((i) => i.id === value?.id);
|
||||
if (draggingIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (draggingIndex === droppingIndex) {
|
||||
const onReorderableDragOver = (target?: DropIdentifier) => {
|
||||
if (!target) {
|
||||
setReorderState((s: ReorderState) => ({
|
||||
...s,
|
||||
reorderedItems: [],
|
||||
}));
|
||||
setA11yMessage(announce.selectedTarget(value.humanData, value.humanData, 'reorder'));
|
||||
setActiveDropTarget(target);
|
||||
return;
|
||||
}
|
||||
const droppingIndex = reorderableGroup.findIndex((i) => i.id === target.id);
|
||||
const draggingIndex = reorderableGroup.findIndex((i) => i.id === value?.id);
|
||||
if (draggingIndex === -1) {
|
||||
return;
|
||||
}
|
||||
setActiveDropTarget(target);
|
||||
|
||||
setA11yMessage(announce.selectedTarget(value.humanData, target.humanData, 'reorder'));
|
||||
|
||||
setReorderState((s: ReorderState) =>
|
||||
draggingIndex < droppingIndex
|
||||
|
@ -561,9 +575,7 @@ const ReorderableDrag = memo(function ReorderableDrag(
|
|||
areItemsReordered
|
||||
? {
|
||||
transform: `translateY(${direction === '+' ? '-' : '+'}${reorderedItems.reduce(
|
||||
(acc, cur) => {
|
||||
return acc + Number(cur.height || 0) + lnsLayerPanelDimensionMargin;
|
||||
},
|
||||
(acc, cur) => acc + Number(cur.height || 0) + lnsLayerPanelDimensionMargin,
|
||||
0
|
||||
)}px)`,
|
||||
}
|
||||
|
@ -572,22 +584,21 @@ const ReorderableDrag = memo(function ReorderableDrag(
|
|||
>
|
||||
<DragInner
|
||||
{...props}
|
||||
ariaDescribedBy="lnsDragDrop-keyboardInstructionsWithReorder"
|
||||
extraKeyboardHandler={extraKeyboardHandler}
|
||||
onDragStart={onReorderableDragStart}
|
||||
onDragEnd={onReorderableDragEnd}
|
||||
onDrop={onReorderableDrop}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const ReorderableDrop = memo(function ReorderableDrop(
|
||||
props: DropInnerProps & { reorderableGroup: DragDropIdentifier[] }
|
||||
props: DropInnerProps & { reorderableGroup: Array<{ id: string }> }
|
||||
) {
|
||||
const {
|
||||
onDrop,
|
||||
value,
|
||||
droppable,
|
||||
dragging,
|
||||
setDragging,
|
||||
setKeyboardMode,
|
||||
|
@ -595,6 +606,7 @@ const ReorderableDrop = memo(function ReorderableDrop(
|
|||
setActiveDropTarget,
|
||||
reorderableGroup,
|
||||
setA11yMessage,
|
||||
dropType,
|
||||
} = props;
|
||||
|
||||
const currentIndex = reorderableGroup.findIndex((i) => i.id === value.id);
|
||||
|
@ -628,15 +640,14 @@ const ReorderableDrop = memo(function ReorderableDrop(
|
|||
}, [isReordered, setReorderState, value.id]);
|
||||
|
||||
const onReorderableDragOver = (e: DroppableEvent) => {
|
||||
if (!droppable) {
|
||||
if (!dropType) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
// An optimization to prevent a bunch of React churn.
|
||||
// todo: replace with custom function ?
|
||||
if (!activeDropTargetMatches) {
|
||||
setActiveDropTarget(value);
|
||||
if (!activeDropTargetMatches && dropType && onDrop) {
|
||||
setActiveDropTarget({ ...value, dropType, onDrop });
|
||||
}
|
||||
|
||||
const draggingIndex = reorderableGroup.findIndex((i) => i.id === dragging?.id);
|
||||
|
@ -675,14 +686,12 @@ const ReorderableDrop = memo(function ReorderableDrop(
|
|||
setDragging(undefined);
|
||||
setKeyboardMode(false);
|
||||
|
||||
if (onDrop && droppable && dragging) {
|
||||
if (onDrop && dropType && dragging) {
|
||||
trackUiEvent('drop_total');
|
||||
|
||||
onDrop(dragging, value);
|
||||
const draggingIndex = reorderableGroup.findIndex((i) => i.id === dragging.id);
|
||||
onDrop(dragging, 'reorder');
|
||||
// setTimeout ensures it will run after dragEnd messaging
|
||||
setTimeout(() =>
|
||||
setA11yMessage(reorderAnnouncements.dropped(currentIndex + 1, draggingIndex + 1))
|
||||
setA11yMessage(announce.dropped(dragging.humanData, value.humanData, 'reorder'))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -707,7 +716,7 @@ const ReorderableDrop = memo(function ReorderableDrop(
|
|||
<div
|
||||
data-test-subj="lnsDragDrop-reorderableDropLayer"
|
||||
className={classNames('lnsDragDrop', {
|
||||
['lnsDragDrop__reorderableDrop']: dragging && droppable,
|
||||
['lnsDragDrop__reorderableDrop']: dragging && dropType,
|
||||
})}
|
||||
onDrop={onReorderableDrop}
|
||||
onDragOver={onReorderableDragOver}
|
||||
|
|
|
@ -9,13 +9,30 @@ import React, { useState, useMemo } from 'react';
|
|||
import classNames from 'classnames';
|
||||
import { EuiScreenReaderOnly, EuiPortal } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { HumanData } from './announcements';
|
||||
import { DropType } from '../types';
|
||||
|
||||
/**
|
||||
* A function that handles a drop event.
|
||||
*/
|
||||
export type DropHandler = (dropped: DragDropIdentifier, dropType?: DropType) => void;
|
||||
|
||||
export type DragDropIdentifier = Record<string, unknown> & {
|
||||
id: string;
|
||||
/**
|
||||
* The data for accessibility, consists of required label and not required groupLabel and position in group
|
||||
*/
|
||||
humanData: HumanData;
|
||||
};
|
||||
|
||||
export interface ActiveDropTarget {
|
||||
activeDropTarget?: DragDropIdentifier;
|
||||
export type DropIdentifier = DragDropIdentifier & {
|
||||
dropType: DropType;
|
||||
onDrop: DropHandler;
|
||||
};
|
||||
|
||||
export interface DropTargets {
|
||||
activeDropTarget?: DropIdentifier;
|
||||
dropTargetsByOrder: Record<string, DropIdentifier | undefined>;
|
||||
}
|
||||
/**
|
||||
* The shape of the drag / drop context.
|
||||
|
@ -39,11 +56,12 @@ export interface DragContextState {
|
|||
*/
|
||||
setDragging: (dragging?: DragDropIdentifier) => void;
|
||||
|
||||
activeDropTarget?: ActiveDropTarget;
|
||||
activeDropTarget?: DropTargets;
|
||||
|
||||
setActiveDropTarget: (newTarget?: DragDropIdentifier) => void;
|
||||
setActiveDropTarget: (newTarget?: DropIdentifier) => void;
|
||||
|
||||
setA11yMessage: (message: string) => void;
|
||||
registerDropTarget: (order: number[], dropTarget?: DropIdentifier) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,6 +77,7 @@ export const DragContext = React.createContext<DragContextState>({
|
|||
activeDropTarget: undefined,
|
||||
setActiveDropTarget: () => {},
|
||||
setA11yMessage: () => {},
|
||||
registerDropTarget: () => {},
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -89,10 +108,13 @@ export interface ProviderProps {
|
|||
setDragging: (dragging?: DragDropIdentifier) => void;
|
||||
|
||||
activeDropTarget?: {
|
||||
activeDropTarget?: DragDropIdentifier;
|
||||
activeDropTarget?: DropIdentifier;
|
||||
dropTargetsByOrder: Record<string, DropIdentifier | undefined>;
|
||||
};
|
||||
|
||||
setActiveDropTarget: (newTarget?: DragDropIdentifier) => void;
|
||||
setActiveDropTarget: (newTarget?: DropIdentifier) => void;
|
||||
|
||||
registerDropTarget: (order: number[], dropTarget?: DropIdentifier) => void;
|
||||
|
||||
/**
|
||||
* The React children.
|
||||
|
@ -116,9 +138,11 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode }
|
|||
const [keyboardModeState, setKeyboardModeState] = useState(false);
|
||||
const [a11yMessageState, setA11yMessageState] = useState('');
|
||||
const [activeDropTargetState, setActiveDropTargetState] = useState<{
|
||||
activeDropTarget?: DragDropIdentifier;
|
||||
activeDropTarget?: DropIdentifier;
|
||||
dropTargetsByOrder: Record<string, DropIdentifier | undefined>;
|
||||
}>({
|
||||
activeDropTarget: undefined,
|
||||
dropTargetsByOrder: {},
|
||||
});
|
||||
|
||||
const setDragging = useMemo(
|
||||
|
@ -131,11 +155,26 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode }
|
|||
]);
|
||||
|
||||
const setActiveDropTarget = useMemo(
|
||||
() => (activeDropTarget?: DragDropIdentifier) =>
|
||||
() => (activeDropTarget?: DropIdentifier) =>
|
||||
setActiveDropTargetState((s) => ({ ...s, activeDropTarget })),
|
||||
[setActiveDropTargetState]
|
||||
);
|
||||
|
||||
const registerDropTarget = useMemo(
|
||||
() => (order: number[], dropTarget?: DropIdentifier) => {
|
||||
return setActiveDropTargetState((s) => {
|
||||
return {
|
||||
...s,
|
||||
dropTargetsByOrder: {
|
||||
...s.dropTargetsByOrder,
|
||||
[order.join(',')]: dropTarget,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[setActiveDropTargetState]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ChildDragDropProvider
|
||||
|
@ -146,6 +185,7 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode }
|
|||
setDragging={setDragging}
|
||||
activeDropTarget={activeDropTargetState}
|
||||
setActiveDropTarget={setActiveDropTarget}
|
||||
registerDropTarget={registerDropTarget}
|
||||
>
|
||||
{children}
|
||||
</ChildDragDropProvider>
|
||||
|
@ -155,9 +195,14 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode }
|
|||
<p aria-live="assertive" aria-atomic={true}>
|
||||
{a11yMessageState}
|
||||
</p>
|
||||
<p id={`lnsDragDrop-keyboardInstructionsWithReorder`}>
|
||||
{i18n.translate('xpack.lens.dragDrop.keyboardInstructionsReorder', {
|
||||
defaultMessage: `Press enter or space to dragging. When dragging, use the up/down arrow keys to reorder items in the group and left/right arrow keys to choose drop targets outside of the group. Press enter or space again to finish.`,
|
||||
})}
|
||||
</p>
|
||||
<p id={`lnsDragDrop-keyboardInstructions`}>
|
||||
{i18n.translate('xpack.lens.dragDrop.keyboardInstructions', {
|
||||
defaultMessage: `Press enter or space to start reordering the dimension group. When dragging, use arrow keys to reorder. Press enter or space again to finish.`,
|
||||
defaultMessage: `Press enter or space to start dragging. When dragging, use the left/right arrow keys to move between drop targets. Press enter or space again to finish.`,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -167,6 +212,45 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode }
|
|||
);
|
||||
}
|
||||
|
||||
export function nextValidDropTarget(
|
||||
activeDropTarget: DropTargets | undefined,
|
||||
draggingOrder: [string],
|
||||
filterElements: (el: DragDropIdentifier) => boolean = () => true,
|
||||
reverse = false
|
||||
) {
|
||||
if (!activeDropTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredTargets = [...Object.entries(activeDropTarget.dropTargetsByOrder)].filter(
|
||||
([, dropTarget]) => dropTarget && filterElements(dropTarget)
|
||||
);
|
||||
|
||||
const nextDropTargets = [...filteredTargets, draggingOrder].sort(([orderA], [orderB]) => {
|
||||
const parsedOrderA = orderA.split(',').map((v) => Number(v));
|
||||
const parsedOrderB = orderB.split(',').map((v) => Number(v));
|
||||
|
||||
const relevantLevel = parsedOrderA.findIndex((v, i) => parsedOrderA[i] !== parsedOrderB[i]);
|
||||
return parsedOrderA[relevantLevel] - parsedOrderB[relevantLevel];
|
||||
});
|
||||
|
||||
let currentActiveDropIndex = nextDropTargets.findIndex(
|
||||
([_, dropTarget]) => dropTarget?.id === activeDropTarget?.activeDropTarget?.id
|
||||
);
|
||||
|
||||
if (currentActiveDropIndex === -1) {
|
||||
currentActiveDropIndex = nextDropTargets.findIndex(
|
||||
([targetOrder]) => targetOrder === draggingOrder[0]
|
||||
);
|
||||
}
|
||||
|
||||
const previousElement =
|
||||
(nextDropTargets.length + currentActiveDropIndex - 1) % nextDropTargets.length;
|
||||
const nextElement = (currentActiveDropIndex + 1) % nextDropTargets.length;
|
||||
|
||||
return nextDropTargets[reverse ? previousElement : nextElement][1];
|
||||
}
|
||||
|
||||
/**
|
||||
* A React drag / drop provider that derives its state from a RootDragDropProvider. If
|
||||
* part of a React application is rendered separately from the root, this provider can
|
||||
|
@ -182,6 +266,7 @@ export function ChildDragDropProvider({
|
|||
activeDropTarget,
|
||||
setActiveDropTarget,
|
||||
setA11yMessage,
|
||||
registerDropTarget,
|
||||
children,
|
||||
}: ProviderProps) {
|
||||
const value = useMemo(
|
||||
|
@ -193,6 +278,7 @@ export function ChildDragDropProvider({
|
|||
activeDropTarget,
|
||||
setActiveDropTarget,
|
||||
setA11yMessage,
|
||||
registerDropTarget,
|
||||
}),
|
||||
[
|
||||
setDragging,
|
||||
|
@ -202,6 +288,7 @@ export function ChildDragDropProvider({
|
|||
setKeyboardMode,
|
||||
keyboardMode,
|
||||
setA11yMessage,
|
||||
registerDropTarget,
|
||||
]
|
||||
);
|
||||
return <DragContext.Provider value={value}>{children}</DragContext.Provider>;
|
||||
|
@ -211,7 +298,7 @@ export interface ReorderState {
|
|||
/**
|
||||
* Ids of the elements that are translated up or down
|
||||
*/
|
||||
reorderedItems: DragDropIdentifier[];
|
||||
reorderedItems: Array<{ id: string; height?: number }>;
|
||||
|
||||
/**
|
||||
* Direction of the move of dragged element in the reordered list
|
||||
|
@ -282,51 +369,3 @@ export function ReorderProvider({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const reorderAnnouncements = {
|
||||
moved: (itemLabel: string, position: number, prevPosition: number) => {
|
||||
return prevPosition === position
|
||||
? i18n.translate('xpack.lens.dragDrop.elementMovedBack', {
|
||||
defaultMessage: `You have moved back the item {itemLabel} to position {prevPosition}`,
|
||||
values: {
|
||||
itemLabel,
|
||||
prevPosition,
|
||||
},
|
||||
})
|
||||
: i18n.translate('xpack.lens.dragDrop.elementMoved', {
|
||||
defaultMessage: `You have moved the item {itemLabel} from position {prevPosition} to position {position}`,
|
||||
values: {
|
||||
itemLabel,
|
||||
position,
|
||||
prevPosition,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
lifted: (itemLabel: string, position: number) =>
|
||||
i18n.translate('xpack.lens.dragDrop.elementLifted', {
|
||||
defaultMessage: `You have lifted an item {itemLabel} in position {position}`,
|
||||
values: {
|
||||
itemLabel,
|
||||
position,
|
||||
},
|
||||
}),
|
||||
|
||||
cancelled: (position: number) =>
|
||||
i18n.translate('xpack.lens.dragDrop.abortMessageReorder', {
|
||||
defaultMessage:
|
||||
'Movement cancelled. The item has returned to its starting position {position}',
|
||||
values: {
|
||||
position,
|
||||
},
|
||||
}),
|
||||
dropped: (position: number, prevPosition: number) =>
|
||||
i18n.translate('xpack.lens.dragDrop.dropMessageReorder', {
|
||||
defaultMessage:
|
||||
'You have dropped the item. You have moved the item from position {prevPosition} to positon {position}',
|
||||
values: {
|
||||
position,
|
||||
prevPosition,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -56,7 +56,7 @@ const { dragging } = useContext(DragContext);
|
|||
return (
|
||||
<DragDrop
|
||||
className="axis"
|
||||
droppable={dragging && canHandleDrop(dragging)}
|
||||
dropType={getDropTypes(dragging)}
|
||||
onDrop={(item) => onChange([...items, item])}
|
||||
>
|
||||
{items.map((x) => (
|
||||
|
@ -86,11 +86,14 @@ The children `DragDrop` components must have props defined as in the example:
|
|||
key={f.id}
|
||||
draggable
|
||||
droppable
|
||||
dragType="reorder"
|
||||
dragType="move"
|
||||
dropType="reorder"
|
||||
reorderableGroup={fields} // consists all reorderable elements in the group, eg. [{id:'3'}, {id:'5'}, {id:'1'}]
|
||||
value={{
|
||||
id: f.id,
|
||||
humanData: {
|
||||
label: 'Label'
|
||||
}
|
||||
}}
|
||||
onDrop={/*handler*/}
|
||||
>
|
||||
|
|
|
@ -7,14 +7,29 @@
|
|||
|
||||
import React, { useMemo } from 'react';
|
||||
import { DragDrop, DragDropIdentifier, DragContextState } from '../../../drag_drop';
|
||||
import { Datasource, VisualizationDimensionGroupConfig, isDraggedOperation } from '../../../types';
|
||||
import {
|
||||
Datasource,
|
||||
VisualizationDimensionGroupConfig,
|
||||
isDraggedOperation,
|
||||
DropType,
|
||||
} from '../../../types';
|
||||
import { LayerDatasourceDropProps } from './types';
|
||||
|
||||
const isFromTheSameGroup = (el1: DragDropIdentifier, el2?: DragDropIdentifier) =>
|
||||
el2 && isDraggedOperation(el2) && el1.groupId === el2.groupId && el1.columnId !== el2.columnId;
|
||||
const getAdditionalClassesOnEnter = (dropType?: string) => {
|
||||
if (
|
||||
dropType === 'field_replace' ||
|
||||
dropType === 'replace_compatible' ||
|
||||
dropType === 'replace_incompatible'
|
||||
) {
|
||||
return 'lnsDragDrop-isReplacing';
|
||||
}
|
||||
};
|
||||
|
||||
const isSelf = (el1: DragDropIdentifier, el2?: DragDropIdentifier) =>
|
||||
isDraggedOperation(el2) && el1.columnId === el2.columnId;
|
||||
const getAdditionalClassesOnDroppable = (dropType?: string) => {
|
||||
if (dropType === 'move_incompatible' || dropType === 'replace_incompatible') {
|
||||
return 'lnsDragDrop-notCompatible';
|
||||
}
|
||||
};
|
||||
|
||||
export function DraggableDimensionButton({
|
||||
layerId,
|
||||
|
@ -34,7 +49,11 @@ export function DraggableDimensionButton({
|
|||
layerId: string;
|
||||
groupIndex: number;
|
||||
layerIndex: number;
|
||||
onDrop: (droppedItem: DragDropIdentifier, dropTarget: DragDropIdentifier) => void;
|
||||
onDrop: (
|
||||
droppedItem: DragDropIdentifier,
|
||||
dropTarget: DragDropIdentifier,
|
||||
dropType?: DropType
|
||||
) => void;
|
||||
group: VisualizationDimensionGroupConfig;
|
||||
label: string;
|
||||
children: React.ReactElement;
|
||||
|
@ -43,66 +62,52 @@ export function DraggableDimensionButton({
|
|||
accessorIndex: number;
|
||||
columnId: string;
|
||||
}) {
|
||||
const value = useMemo(() => {
|
||||
return {
|
||||
const dropType = layerDatasource.getDropTypes({
|
||||
...layerDatasourceDropProps,
|
||||
columnId,
|
||||
filterOperations: group.filterOperations,
|
||||
groupId: group.groupId,
|
||||
});
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
columnId,
|
||||
groupId: group.groupId,
|
||||
layerId,
|
||||
id: columnId,
|
||||
};
|
||||
}, [columnId, group.groupId, layerId]);
|
||||
|
||||
const { dragging } = dragDropContext;
|
||||
|
||||
const isCurrentGroup = group.groupId === dragging?.groupId;
|
||||
const isOperationDragged = isDraggedOperation(dragging);
|
||||
const canHandleDrop =
|
||||
Boolean(dragDropContext.dragging) &&
|
||||
layerDatasource.canHandleDrop({
|
||||
...layerDatasourceDropProps,
|
||||
columnId,
|
||||
filterOperations: group.filterOperations,
|
||||
});
|
||||
|
||||
const dragType = isSelf(value, dragging)
|
||||
? 'move'
|
||||
: isOperationDragged && isCurrentGroup
|
||||
? 'reorder'
|
||||
: 'copy';
|
||||
|
||||
const dropType = isOperationDragged ? (!isCurrentGroup ? 'replace' : 'reorder') : 'add';
|
||||
|
||||
const isCompatibleFromOtherGroup = !isCurrentGroup && canHandleDrop;
|
||||
|
||||
const isDroppable = isOperationDragged
|
||||
? dragType === 'reorder'
|
||||
? isFromTheSameGroup(value, dragging)
|
||||
: isCompatibleFromOtherGroup
|
||||
: canHandleDrop;
|
||||
dropType,
|
||||
humanData: {
|
||||
label,
|
||||
groupLabel: group.groupLabel,
|
||||
position: accessorIndex + 1,
|
||||
},
|
||||
}),
|
||||
[columnId, group.groupId, accessorIndex, layerId, dropType, label, group.groupLabel]
|
||||
);
|
||||
|
||||
// todo: simplify by id and use drop targets?
|
||||
const reorderableGroup = useMemo(
|
||||
() =>
|
||||
group.accessors.map((a) => ({
|
||||
columnId: a.columnId,
|
||||
id: a.columnId,
|
||||
groupId: group.groupId,
|
||||
layerId,
|
||||
group.accessors.map((g) => ({
|
||||
id: g.columnId,
|
||||
})),
|
||||
[group, layerId]
|
||||
[group.accessors]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="lnsLayerPanel__dimensionContainer" data-test-subj={group.dataTestSubj}>
|
||||
<DragDrop
|
||||
noKeyboardSupportYet={reorderableGroup.length < 2} // to be removed when navigating outside of groups is added
|
||||
getAdditionalClassesOnEnter={getAdditionalClassesOnEnter}
|
||||
getAdditionalClassesOnDroppable={getAdditionalClassesOnDroppable}
|
||||
order={[2, layerIndex, groupIndex, accessorIndex]}
|
||||
draggable
|
||||
dragType={dragType}
|
||||
dragType={isDraggedOperation(dragDropContext.dragging) ? 'move' : 'copy'}
|
||||
dropType={dropType}
|
||||
reorderableGroup={reorderableGroup.length > 1 ? reorderableGroup : undefined}
|
||||
value={value}
|
||||
label={label}
|
||||
droppable={dragging && isDroppable}
|
||||
onDrop={onDrop}
|
||||
onDrop={(drag: DragDropIdentifier, selectedDropType?: DropType) =>
|
||||
onDrop(drag, value, selectedDropType)
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</DragDrop>
|
||||
|
|
|
@ -5,17 +5,26 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useState, useEffect } from 'react';
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { generateId } from '../../../id_generator';
|
||||
import { DragDrop, DragDropIdentifier, DragContextState } from '../../../drag_drop';
|
||||
import { Datasource, VisualizationDimensionGroupConfig, isDraggedOperation } from '../../../types';
|
||||
import { DragDrop, DragDropIdentifier } from '../../../drag_drop';
|
||||
import { Datasource, VisualizationDimensionGroupConfig, DropType } from '../../../types';
|
||||
import { LayerDatasourceDropProps } from './types';
|
||||
|
||||
const label = i18n.translate('xpack.lens.indexPattern.emptyDimensionButton', {
|
||||
defaultMessage: 'Empty dimension',
|
||||
});
|
||||
|
||||
const getAdditionalClassesOnDroppable = (dropType?: string) => {
|
||||
if (dropType === 'move_incompatible' || dropType === 'replace_incompatible') {
|
||||
return 'lnsDragDrop-notCompatible';
|
||||
}
|
||||
};
|
||||
|
||||
export function EmptyDimensionButton({
|
||||
dragDropContext,
|
||||
group,
|
||||
layerDatasource,
|
||||
layerDatasourceDropProps,
|
||||
|
@ -25,48 +34,58 @@ export function EmptyDimensionButton({
|
|||
onClick,
|
||||
onDrop,
|
||||
}: {
|
||||
dragDropContext: DragContextState;
|
||||
layerId: string;
|
||||
groupIndex: number;
|
||||
layerIndex: number;
|
||||
onClick: (id: string) => void;
|
||||
onDrop: (droppedItem: DragDropIdentifier, dropTarget: DragDropIdentifier) => void;
|
||||
onDrop: (
|
||||
droppedItem: DragDropIdentifier,
|
||||
dropTarget: DragDropIdentifier,
|
||||
dropType?: DropType
|
||||
) => void;
|
||||
group: VisualizationDimensionGroupConfig;
|
||||
|
||||
layerDatasource: Datasource<unknown, unknown>;
|
||||
layerDatasourceDropProps: LayerDatasourceDropProps;
|
||||
}) {
|
||||
const handleDrop = (droppedItem: DragDropIdentifier) => onDrop(droppedItem, value);
|
||||
const itemIndex = group.accessors.length;
|
||||
|
||||
const value = useMemo(() => {
|
||||
const newId = generateId();
|
||||
return {
|
||||
columnId: newId,
|
||||
const [newColumnId, setNewColumnId] = useState<string>(generateId());
|
||||
useEffect(() => {
|
||||
setNewColumnId(generateId());
|
||||
}, [itemIndex]);
|
||||
|
||||
const dropType = layerDatasource.getDropTypes({
|
||||
...layerDatasourceDropProps,
|
||||
columnId: newColumnId,
|
||||
filterOperations: group.filterOperations,
|
||||
groupId: group.groupId,
|
||||
});
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
columnId: newColumnId,
|
||||
groupId: group.groupId,
|
||||
layerId,
|
||||
isNew: true,
|
||||
id: newId,
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [group.accessors.length, group.groupId, layerId]);
|
||||
id: newColumnId,
|
||||
dropType,
|
||||
humanData: {
|
||||
label,
|
||||
groupLabel: group.groupLabel,
|
||||
position: itemIndex + 1,
|
||||
},
|
||||
}),
|
||||
[dropType, newColumnId, group.groupId, layerId, group.groupLabel, itemIndex]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="lnsLayerPanel__dimensionContainer" data-test-subj={group.dataTestSubj}>
|
||||
<DragDrop
|
||||
getAdditionalClassesOnDroppable={getAdditionalClassesOnDroppable}
|
||||
value={value}
|
||||
onDrop={handleDrop}
|
||||
droppable={
|
||||
Boolean(dragDropContext.dragging) &&
|
||||
// Verify that the dragged item is not coming from the same group
|
||||
// since this would be a duplicate
|
||||
(!isDraggedOperation(dragDropContext.dragging) ||
|
||||
dragDropContext.dragging.groupId !== group.groupId) &&
|
||||
layerDatasource.canHandleDrop({
|
||||
...layerDatasourceDropProps,
|
||||
columnId: value.columnId,
|
||||
filterOperations: group.filterOperations,
|
||||
})
|
||||
}
|
||||
/* 2 to leave room for data panel and workspace, then go by layer index, then by group index */
|
||||
order={[2, layerIndex, groupIndex, itemIndex]}
|
||||
onDrop={(droppedItem, selectedDropType) => onDrop(droppedItem, value, selectedDropType)}
|
||||
dropType={dropType}
|
||||
>
|
||||
<div className="lnsLayerPanel__dimension lnsLayerPanel__dimension--empty">
|
||||
<EuiButtonEmpty
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
createMockDatasource,
|
||||
DatasourceMock,
|
||||
} from '../../mocks';
|
||||
import { ChildDragDropProvider, DroppableEvent, DragDrop } from '../../../drag_drop';
|
||||
import { ChildDragDropProvider, DragDrop } from '../../../drag_drop';
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { mountWithIntl } from '@kbn/test/jest';
|
||||
import { Visualization } from '../../../types';
|
||||
|
@ -31,6 +31,7 @@ const defaultContext = {
|
|||
keyboardMode: false,
|
||||
setKeyboardMode: () => {},
|
||||
setA11yMessage: jest.fn(),
|
||||
registerDropTarget: jest.fn(),
|
||||
};
|
||||
|
||||
describe('LayerPanel', () => {
|
||||
|
@ -224,7 +225,7 @@ describe('LayerPanel', () => {
|
|||
});
|
||||
|
||||
it('should not update the visualization if the datasource is incomplete', () => {
|
||||
(generateId as jest.Mock).mockReturnValueOnce(`newid`);
|
||||
(generateId as jest.Mock).mockReturnValue(`newid`);
|
||||
const updateAll = jest.fn();
|
||||
const updateDatasource = jest.fn();
|
||||
|
||||
|
@ -439,9 +440,14 @@ describe('LayerPanel', () => {
|
|||
],
|
||||
});
|
||||
|
||||
mockDatasource.canHandleDrop.mockReturnValue(true);
|
||||
mockDatasource.getDropTypes.mockReturnValue('field_add');
|
||||
|
||||
const draggingField = { field: { name: 'dragged' }, indexPatternId: 'a', id: '1' };
|
||||
const draggingField = {
|
||||
field: { name: 'dragged' },
|
||||
indexPatternId: 'a',
|
||||
id: '1',
|
||||
humanData: { label: 'Label' },
|
||||
};
|
||||
|
||||
const component = mountWithIntl(
|
||||
<ChildDragDropProvider {...defaultContext} dragging={draggingField}>
|
||||
|
@ -449,7 +455,7 @@ describe('LayerPanel', () => {
|
|||
</ChildDragDropProvider>
|
||||
);
|
||||
|
||||
expect(mockDatasource.canHandleDrop).toHaveBeenCalledWith(
|
||||
expect(mockDatasource.getDropTypes).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dragDropContext: expect.objectContaining({
|
||||
dragging: draggingField,
|
||||
|
@ -482,9 +488,16 @@ describe('LayerPanel', () => {
|
|||
],
|
||||
});
|
||||
|
||||
mockDatasource.canHandleDrop.mockImplementation(({ columnId }) => columnId !== 'a');
|
||||
mockDatasource.getDropTypes.mockImplementation(({ columnId }) =>
|
||||
columnId !== 'a' ? 'field_replace' : undefined
|
||||
);
|
||||
|
||||
const draggingField = { field: { name: 'dragged' }, indexPatternId: 'a', id: '1' };
|
||||
const draggingField = {
|
||||
field: { name: 'dragged' },
|
||||
indexPatternId: 'a',
|
||||
id: '1',
|
||||
humanData: { label: 'Label' },
|
||||
};
|
||||
|
||||
const component = mountWithIntl(
|
||||
<ChildDragDropProvider {...defaultContext} dragging={draggingField}>
|
||||
|
@ -492,13 +505,13 @@ describe('LayerPanel', () => {
|
|||
</ChildDragDropProvider>
|
||||
);
|
||||
|
||||
expect(mockDatasource.canHandleDrop).toHaveBeenCalledWith(
|
||||
expect(mockDatasource.getDropTypes).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ columnId: 'a' })
|
||||
);
|
||||
|
||||
expect(
|
||||
component.find('[data-test-subj="lnsGroup"] DragDrop').first().prop('droppable')
|
||||
).toEqual(false);
|
||||
component.find('[data-test-subj="lnsGroup"] DragDrop').first().prop('dropType')
|
||||
).toEqual(undefined);
|
||||
|
||||
component
|
||||
.find('[data-test-subj="lnsGroup"] DragDrop')
|
||||
|
@ -533,9 +546,15 @@ describe('LayerPanel', () => {
|
|||
],
|
||||
});
|
||||
|
||||
mockDatasource.canHandleDrop.mockReturnValue(true);
|
||||
mockDatasource.getDropTypes.mockReturnValue('replace_compatible');
|
||||
|
||||
const draggingOperation = { layerId: 'first', columnId: 'a', groupId: 'a', id: 'a' };
|
||||
const draggingOperation = {
|
||||
layerId: 'first',
|
||||
columnId: 'a',
|
||||
groupId: 'a',
|
||||
id: 'a',
|
||||
humanData: { label: 'Label' },
|
||||
};
|
||||
|
||||
const component = mountWithIntl(
|
||||
<ChildDragDropProvider {...defaultContext} dragging={draggingOperation}>
|
||||
|
@ -543,7 +562,7 @@ describe('LayerPanel', () => {
|
|||
</ChildDragDropProvider>
|
||||
);
|
||||
|
||||
expect(mockDatasource.canHandleDrop).toHaveBeenCalledWith(
|
||||
expect(mockDatasource.getDropTypes).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dragDropContext: expect.objectContaining({
|
||||
dragging: draggingOperation,
|
||||
|
@ -588,7 +607,13 @@ describe('LayerPanel', () => {
|
|||
],
|
||||
});
|
||||
|
||||
const draggingOperation = { layerId: 'first', columnId: 'a', groupId: 'a', id: 'a' };
|
||||
const draggingOperation = {
|
||||
layerId: 'first',
|
||||
columnId: 'a',
|
||||
groupId: 'a',
|
||||
id: 'a',
|
||||
humanData: { label: 'Label' },
|
||||
};
|
||||
|
||||
const component = mountWithIntl(
|
||||
<ChildDragDropProvider {...defaultContext} dragging={draggingOperation}>
|
||||
|
@ -596,15 +621,10 @@ describe('LayerPanel', () => {
|
|||
</ChildDragDropProvider>
|
||||
);
|
||||
|
||||
component.find(DragDrop).at(1).prop('onDrop')!(draggingOperation, {
|
||||
layerId: 'first',
|
||||
columnId: 'b',
|
||||
groupId: 'a',
|
||||
id: 'b',
|
||||
});
|
||||
component.find(DragDrop).at(1).prop('onDrop')!(draggingOperation, 'reorder');
|
||||
expect(mockDatasource.onDrop).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
groupId: 'a',
|
||||
dropType: 'reorder',
|
||||
droppedItem: draggingOperation,
|
||||
})
|
||||
);
|
||||
|
@ -624,22 +644,24 @@ describe('LayerPanel', () => {
|
|||
],
|
||||
});
|
||||
|
||||
const draggingOperation = { layerId: 'first', columnId: 'a', groupId: 'a', id: 'a' };
|
||||
const draggingOperation = {
|
||||
layerId: 'first',
|
||||
columnId: 'a',
|
||||
groupId: 'a',
|
||||
id: 'a',
|
||||
humanData: { label: 'Label' },
|
||||
};
|
||||
|
||||
const component = mountWithIntl(
|
||||
<ChildDragDropProvider {...defaultContext} dragging={draggingOperation}>
|
||||
<LayerPanel {...getDefaultProps()} />
|
||||
</ChildDragDropProvider>
|
||||
);
|
||||
|
||||
component.find('[data-test-subj="lnsGroup"] DragDrop').at(2).prop('onDrop')!(
|
||||
(draggingOperation as unknown) as DroppableEvent
|
||||
);
|
||||
component.find(DragDrop).at(2).prop('onDrop')!(draggingOperation, 'duplicate_in_group');
|
||||
expect(mockDatasource.onDrop).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
groupId: 'a',
|
||||
dropType: 'duplicate_in_group',
|
||||
droppedItem: draggingOperation,
|
||||
isNew: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ import React, { useContext, useState, useEffect, useMemo, useCallback } from 're
|
|||
import { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { NativeRenderer } from '../../../native_renderer';
|
||||
import { StateSetter, Visualization } from '../../../types';
|
||||
import { StateSetter, Visualization, DraggedOperation, DropType } from '../../../types';
|
||||
import {
|
||||
DragContext,
|
||||
DragDropIdentifier,
|
||||
|
@ -115,13 +115,19 @@ export function LayerPanel(
|
|||
const layerDatasourceOnDrop = layerDatasource.onDrop;
|
||||
|
||||
const onDrop = useMemo(() => {
|
||||
return (droppedItem: DragDropIdentifier, targetItem: DragDropIdentifier) => {
|
||||
const { columnId, groupId, layerId: targetLayerId, isNew } = (targetItem as unknown) as {
|
||||
groupId: string;
|
||||
columnId: string;
|
||||
layerId: string;
|
||||
isNew?: boolean;
|
||||
};
|
||||
return (
|
||||
droppedItem: DragDropIdentifier,
|
||||
targetItem: DragDropIdentifier,
|
||||
dropType?: DropType
|
||||
) => {
|
||||
if (!dropType) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
columnId,
|
||||
groupId,
|
||||
layerId: targetLayerId,
|
||||
} = (targetItem as unknown) as DraggedOperation; // TODO: correct misleading name
|
||||
|
||||
const filterOperations =
|
||||
groups.find(({ groupId: gId }) => gId === targetItem.groupId)?.filterOperations ||
|
||||
|
@ -131,10 +137,9 @@ export function LayerPanel(
|
|||
...layerDatasourceDropProps,
|
||||
droppedItem,
|
||||
columnId,
|
||||
groupId,
|
||||
layerId: targetLayerId,
|
||||
isNew,
|
||||
filterOperations,
|
||||
dropType,
|
||||
});
|
||||
if (dropResult) {
|
||||
updateVisualization(
|
||||
|
@ -317,7 +322,6 @@ export function LayerPanel(
|
|||
</ReorderProvider>
|
||||
{group.supportsMoreColumns ? (
|
||||
<EmptyDimensionButton
|
||||
dragDropContext={dragDropContext}
|
||||
group={group}
|
||||
groupIndex={groupIndex}
|
||||
layerId={layerId}
|
||||
|
|
|
@ -1323,7 +1323,7 @@ describe('editor_frame', () => {
|
|||
getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()],
|
||||
renderDataPanel: (_element, { dragDropContext: { setDragging, dragging } }) => {
|
||||
if (!dragging || dragging.id !== 'draggedField') {
|
||||
setDragging({ id: 'draggedField' });
|
||||
setDragging({ id: 'draggedField', humanData: { label: 'draggedField' } });
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -1344,8 +1344,9 @@ describe('editor_frame', () => {
|
|||
indexPatternId: '1',
|
||||
field: {},
|
||||
id: '1',
|
||||
humanData: { label: 'draggedField' },
|
||||
},
|
||||
{ id: 'lnsWorkspace' }
|
||||
'field_replace'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1424,7 +1425,7 @@ describe('editor_frame', () => {
|
|||
getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()],
|
||||
renderDataPanel: (_element, { dragDropContext: { setDragging, dragging } }) => {
|
||||
if (!dragging || dragging.id !== 'draggedField') {
|
||||
setDragging({ id: 'draggedField' });
|
||||
setDragging({ id: 'draggedField', humanData: { label: '1' } });
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -1445,8 +1446,11 @@ describe('editor_frame', () => {
|
|||
indexPatternId: '1',
|
||||
field: {},
|
||||
id: '1',
|
||||
humanData: {
|
||||
label: 'label',
|
||||
},
|
||||
},
|
||||
{ id: 'lnsWorkspace' }
|
||||
'field_replace'
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -532,7 +532,7 @@ describe('suggestion helpers', () => {
|
|||
{
|
||||
mockindexpattern: { state: mockDatasourceState, isLoading: false },
|
||||
},
|
||||
{ id: 'myfield' },
|
||||
{ id: 'myfield', humanData: { label: 'myfieldLabel' } },
|
||||
];
|
||||
});
|
||||
|
||||
|
@ -543,6 +543,9 @@ describe('suggestion helpers', () => {
|
|||
mockDatasourceState,
|
||||
{
|
||||
id: 'myfield',
|
||||
humanData: {
|
||||
label: 'myfieldLabel',
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -775,7 +775,7 @@ describe('workspace_panel', () => {
|
|||
let mockGetSuggestionForField: jest.Mock;
|
||||
let frame: jest.Mocked<FramePublicAPI>;
|
||||
|
||||
const draggedField = { id: 'field' };
|
||||
const draggedField = { id: 'field', humanData: { label: 'Label' } };
|
||||
|
||||
beforeEach(() => {
|
||||
frame = createMockFramePublicAPI();
|
||||
|
@ -793,6 +793,7 @@ describe('workspace_panel', () => {
|
|||
keyboardMode={false}
|
||||
setKeyboardMode={() => {}}
|
||||
setA11yMessage={() => {}}
|
||||
registerDropTarget={jest.fn()}
|
||||
>
|
||||
<WorkspacePanel
|
||||
activeDatasourceId={'mock'}
|
||||
|
@ -831,7 +832,7 @@ describe('workspace_panel', () => {
|
|||
});
|
||||
initComponent();
|
||||
|
||||
instance.find(DragDrop).prop('onDrop')!(draggedField, { id: 'lnsWorkspace' });
|
||||
instance.find(DragDrop).prop('onDrop')!(draggedField, 'field_replace');
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalledWith({
|
||||
type: 'SWITCH_VISUALIZATION',
|
||||
|
@ -850,12 +851,12 @@ describe('workspace_panel', () => {
|
|||
visualizationState: {},
|
||||
});
|
||||
initComponent();
|
||||
expect(instance.find(DragDrop).prop('droppable')).toBeTruthy();
|
||||
expect(instance.find(DragDrop).prop('dropType')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should refuse to drop if there are no suggestions', () => {
|
||||
initComponent();
|
||||
expect(instance.find(DragDrop).prop('droppable')).toBeFalsy();
|
||||
expect(instance.find(DragDrop).prop('dropType')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -84,7 +84,17 @@ interface WorkspaceState {
|
|||
expandError: boolean;
|
||||
}
|
||||
|
||||
const workspaceDropValue = { id: 'lnsWorkspace' };
|
||||
const dropProps = {
|
||||
value: {
|
||||
id: 'lnsWorkspace',
|
||||
humanData: {
|
||||
label: i18n.translate('xpack.lens.editorFrame.workspaceLabel', {
|
||||
defaultMessage: 'Workspace',
|
||||
}),
|
||||
},
|
||||
},
|
||||
order: [1, 0, 0, 0],
|
||||
};
|
||||
|
||||
// Exported for testing purposes only.
|
||||
export const WorkspacePanel = React.memo(function WorkspacePanel({
|
||||
|
@ -302,9 +312,10 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({
|
|||
className="lnsWorkspacePanel__dragDrop"
|
||||
dataTestSubj="lnsWorkspace"
|
||||
draggable={false}
|
||||
droppable={Boolean(suggestionForDraggedField)}
|
||||
dropType={suggestionForDraggedField ? 'field_add' : undefined}
|
||||
onDrop={onDrop}
|
||||
value={workspaceDropValue}
|
||||
value={dropProps.value}
|
||||
order={dropProps.order}
|
||||
>
|
||||
<div>
|
||||
{renderVisualization()}
|
||||
|
|
|
@ -88,7 +88,7 @@ export function createMockDatasource(id: string): DatasourceMock {
|
|||
uniqueLabels: jest.fn((_state) => ({})),
|
||||
renderDimensionTrigger: jest.fn(),
|
||||
renderDimensionEditor: jest.fn(),
|
||||
canHandleDrop: jest.fn(),
|
||||
getDropTypes: jest.fn(),
|
||||
onDrop: jest.fn(),
|
||||
|
||||
// this is an additional property which doesn't exist on real datasources
|
||||
|
|
|
@ -281,7 +281,7 @@ describe('IndexPattern Data Panel', () => {
|
|||
setState={setStateSpy}
|
||||
dragDropContext={{
|
||||
...createMockedDragDropContext(),
|
||||
dragging: { id: '1' },
|
||||
dragging: { id: '1', humanData: { label: 'Label' } },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -303,7 +303,7 @@ describe('IndexPattern Data Panel', () => {
|
|||
setState={jest.fn()}
|
||||
dragDropContext={{
|
||||
...createMockedDragDropContext(),
|
||||
dragging: { id: '1' },
|
||||
dragging: { id: '1', humanData: { label: 'Label' } },
|
||||
}}
|
||||
changeIndexPattern={jest.fn()}
|
||||
/>
|
||||
|
@ -338,7 +338,7 @@ describe('IndexPattern Data Panel', () => {
|
|||
setState,
|
||||
dragDropContext: {
|
||||
...createMockedDragDropContext(),
|
||||
dragging: { id: '1' },
|
||||
dragging: { id: '1', humanData: { label: 'Label' } },
|
||||
},
|
||||
dateRange: { fromDate: '2019-01-01', toDate: '2020-01-01' },
|
||||
state: {
|
||||
|
|
|
@ -106,9 +106,6 @@ const bytesColumn: IndexPatternColumn = {
|
|||
*
|
||||
* - Dimension trigger: Not tested here
|
||||
* - Dimension editor component: First half of the tests
|
||||
*
|
||||
* - canHandleDrop: Tests for dropping of fields or other dimensions
|
||||
* - onDrop: Correct application of drop logic
|
||||
*/
|
||||
describe('IndexPatternDimensionEditorPanel', () => {
|
||||
let state: IndexPatternPrivateState;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -12,39 +12,46 @@ import {
|
|||
DraggedOperation,
|
||||
} from '../../types';
|
||||
import { IndexPatternColumn } from '../indexpattern';
|
||||
import { insertOrReplaceColumn } from '../operations';
|
||||
import { insertOrReplaceColumn, deleteColumn } from '../operations';
|
||||
import { mergeLayer } from '../state_helpers';
|
||||
import { hasField, isDraggedField } from '../utils';
|
||||
import { IndexPatternPrivateState, IndexPatternField } from '../types';
|
||||
import { IndexPatternPrivateState, IndexPatternField, DraggedField } from '../types';
|
||||
import { trackUiEvent } from '../../lens_ui_telemetry';
|
||||
import { getOperationSupportMatrix, OperationSupportMatrix } from './operation_support';
|
||||
import { getOperationSupportMatrix } from './operation_support';
|
||||
|
||||
type DropHandlerProps<T = DraggedOperation> = Pick<
|
||||
DatasourceDimensionDropHandlerProps<IndexPatternPrivateState>,
|
||||
'columnId' | 'setState' | 'state' | 'layerId' | 'droppedItem'
|
||||
> & {
|
||||
type DropHandlerProps<T> = DatasourceDimensionDropHandlerProps<IndexPatternPrivateState> & {
|
||||
droppedItem: T;
|
||||
operationSupportMatrix: OperationSupportMatrix;
|
||||
};
|
||||
|
||||
export function canHandleDrop(props: DatasourceDimensionDropProps<IndexPatternPrivateState>) {
|
||||
const operationSupportMatrix = getOperationSupportMatrix(props);
|
||||
|
||||
export function getDropTypes(
|
||||
props: DatasourceDimensionDropProps<IndexPatternPrivateState> & { groupId: string }
|
||||
) {
|
||||
const { dragging } = props.dragDropContext;
|
||||
if (!dragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
const layerIndexPatternId = props.state.layers[props.layerId].indexPatternId;
|
||||
|
||||
function hasOperationForField(field: IndexPatternField) {
|
||||
return Boolean(operationSupportMatrix.operationByField[field.name]);
|
||||
return !!getOperationSupportMatrix(props).operationByField[field.name];
|
||||
}
|
||||
|
||||
const currentColumn = props.state.layers[props.layerId].columns[props.columnId];
|
||||
if (isDraggedField(dragging)) {
|
||||
const currentColumn = props.state.layers[props.layerId].columns[props.columnId];
|
||||
return Boolean(
|
||||
layerIndexPatternId === dragging.indexPatternId &&
|
||||
Boolean(hasOperationForField(dragging.field)) &&
|
||||
(!currentColumn ||
|
||||
(hasField(currentColumn) && currentColumn.sourceField !== dragging.field.name))
|
||||
);
|
||||
if (
|
||||
!!(layerIndexPatternId === dragging.indexPatternId && hasOperationForField(dragging.field))
|
||||
) {
|
||||
if (!currentColumn) {
|
||||
return 'field_add';
|
||||
} else if (
|
||||
(hasField(currentColumn) && currentColumn.sourceField !== dragging.field.name) ||
|
||||
!hasField(currentColumn)
|
||||
) {
|
||||
return 'field_replace';
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -52,12 +59,72 @@ export function canHandleDrop(props: DatasourceDimensionDropProps<IndexPatternPr
|
|||
dragging.layerId === props.layerId &&
|
||||
props.columnId !== dragging.columnId
|
||||
) {
|
||||
const op = props.state.layers[props.layerId].columns[dragging.columnId];
|
||||
return props.filterOperations(op);
|
||||
// same group
|
||||
if (props.groupId === dragging.groupId) {
|
||||
if (currentColumn) {
|
||||
return 'reorder';
|
||||
}
|
||||
return 'duplicate_in_group';
|
||||
}
|
||||
|
||||
// compatible group
|
||||
const op = props.state.layers[dragging.layerId].columns[dragging.columnId];
|
||||
if (
|
||||
!op ||
|
||||
(currentColumn &&
|
||||
hasField(currentColumn) &&
|
||||
hasField(op) &&
|
||||
currentColumn.sourceField === op.sourceField)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (props.filterOperations(op)) {
|
||||
if (currentColumn) {
|
||||
return 'replace_compatible'; // in the future also 'swap_compatible' and 'duplicate_compatible'
|
||||
} else {
|
||||
return 'move_compatible'; // in the future also 'duplicate_compatible'
|
||||
}
|
||||
}
|
||||
|
||||
// suggest
|
||||
const field =
|
||||
hasField(op) && props.state.indexPatterns[layerIndexPatternId].getFieldByName(op.sourceField);
|
||||
if (field && hasOperationForField(field)) {
|
||||
if (currentColumn) {
|
||||
return 'replace_incompatible'; // in the future also 'swap_incompatible', 'duplicate_incompatible'
|
||||
} else {
|
||||
return 'move_incompatible'; // in the future also 'duplicate_incompatible'
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function onDrop(props: DatasourceDimensionDropHandlerProps<IndexPatternPrivateState>) {
|
||||
const { droppedItem, dropType } = props;
|
||||
|
||||
if (dropType === 'field_add' || dropType === 'field_replace') {
|
||||
return operationOnDropMap[dropType]({
|
||||
...props,
|
||||
droppedItem: droppedItem as DraggedField,
|
||||
});
|
||||
}
|
||||
return operationOnDropMap[dropType]({
|
||||
...props,
|
||||
droppedItem: droppedItem as DraggedOperation,
|
||||
});
|
||||
}
|
||||
|
||||
const operationOnDropMap = {
|
||||
field_add: onFieldDrop,
|
||||
field_replace: onFieldDrop,
|
||||
reorder: onReorderDrop,
|
||||
duplicate_in_group: onSameGroupDuplicateDrop,
|
||||
move_compatible: onMoveDropToCompatibleGroup,
|
||||
replace_compatible: onMoveDropToCompatibleGroup,
|
||||
move_incompatible: onMoveDropToNonCompatibleGroup,
|
||||
replace_incompatible: onMoveDropToNonCompatibleGroup,
|
||||
};
|
||||
|
||||
function reorderElements(items: string[], dest: string, src: string) {
|
||||
const result = items.filter((c) => c !== src);
|
||||
const destIndex = items.findIndex((c) => c === src);
|
||||
|
@ -69,7 +136,13 @@ function reorderElements(items: string[], dest: string, src: string) {
|
|||
return result;
|
||||
}
|
||||
|
||||
const onReorderDrop = ({ columnId, setState, state, layerId, droppedItem }: DropHandlerProps) => {
|
||||
function onReorderDrop({
|
||||
columnId,
|
||||
setState,
|
||||
state,
|
||||
layerId,
|
||||
droppedItem,
|
||||
}: DropHandlerProps<DraggedOperation>) {
|
||||
setState(
|
||||
mergeLayer({
|
||||
state,
|
||||
|
@ -85,15 +158,98 @@ const onReorderDrop = ({ columnId, setState, state, layerId, droppedItem }: Drop
|
|||
);
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
const onMoveDropToCompatibleGroup = ({
|
||||
function onMoveDropToNonCompatibleGroup(props: DropHandlerProps<DraggedOperation>) {
|
||||
const { columnId, setState, state, layerId, droppedItem } = props;
|
||||
|
||||
const layer = state.layers[layerId];
|
||||
const op = { ...layer.columns[droppedItem.columnId] };
|
||||
const field =
|
||||
hasField(op) && state.indexPatterns[layer.indexPatternId].getFieldByName(op.sourceField);
|
||||
if (!field) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const operationSupportMatrix = getOperationSupportMatrix(props);
|
||||
const operationsForNewField = operationSupportMatrix.operationByField[field.name];
|
||||
|
||||
if (!operationsForNewField || operationsForNewField.size === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentIndexPattern = state.indexPatterns[layer.indexPatternId];
|
||||
|
||||
const newLayer = insertOrReplaceColumn({
|
||||
layer: deleteColumn({
|
||||
layer,
|
||||
columnId: droppedItem.columnId,
|
||||
indexPattern: currentIndexPattern,
|
||||
}),
|
||||
columnId,
|
||||
indexPattern: currentIndexPattern,
|
||||
op: operationsForNewField.values().next().value,
|
||||
field,
|
||||
});
|
||||
|
||||
trackUiEvent('drop_onto_dimension');
|
||||
setState(
|
||||
mergeLayer({
|
||||
state,
|
||||
layerId,
|
||||
newLayer: {
|
||||
...newLayer,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return { deleted: droppedItem.columnId };
|
||||
}
|
||||
|
||||
function onSameGroupDuplicateDrop({
|
||||
columnId,
|
||||
setState,
|
||||
state,
|
||||
layerId,
|
||||
droppedItem,
|
||||
}: DropHandlerProps) => {
|
||||
}: DropHandlerProps<DraggedOperation>) {
|
||||
const layer = state.layers[layerId];
|
||||
|
||||
const op = { ...layer.columns[droppedItem.columnId] };
|
||||
const newColumns = {
|
||||
...layer.columns,
|
||||
[columnId]: op,
|
||||
};
|
||||
|
||||
const newColumnOrder = [...layer.columnOrder];
|
||||
// put a new bucketed dimension just in front of the metric dimensions, a metric dimension in the back of the array
|
||||
// TODO this logic does not take into account groups - we probably need to pass the current
|
||||
// group config to this position to place the column right
|
||||
const insertionIndex = op.isBucketed
|
||||
? newColumnOrder.findIndex((id) => !newColumns[id].isBucketed)
|
||||
: newColumnOrder.length;
|
||||
newColumnOrder.splice(insertionIndex, 0, columnId);
|
||||
// Time to replace
|
||||
setState(
|
||||
mergeLayer({
|
||||
state,
|
||||
layerId,
|
||||
newLayer: {
|
||||
columnOrder: newColumnOrder,
|
||||
columns: newColumns,
|
||||
},
|
||||
})
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
function onMoveDropToCompatibleGroup({
|
||||
columnId,
|
||||
setState,
|
||||
state,
|
||||
layerId,
|
||||
droppedItem,
|
||||
}: DropHandlerProps<DraggedOperation>) {
|
||||
const layer = state.layers[layerId];
|
||||
const op = { ...layer.columns[droppedItem.columnId] };
|
||||
const newColumns = { ...layer.columns };
|
||||
|
@ -122,18 +278,14 @@ const onMoveDropToCompatibleGroup = ({
|
|||
})
|
||||
);
|
||||
return { deleted: droppedItem.columnId };
|
||||
};
|
||||
}
|
||||
|
||||
function onFieldDrop(props: DropHandlerProps<DraggedField>) {
|
||||
const { columnId, setState, state, layerId, droppedItem } = props;
|
||||
const operationSupportMatrix = getOperationSupportMatrix(props);
|
||||
|
||||
const onFieldDrop = ({
|
||||
columnId,
|
||||
setState,
|
||||
state,
|
||||
layerId,
|
||||
droppedItem,
|
||||
operationSupportMatrix,
|
||||
}: DropHandlerProps<unknown>) => {
|
||||
function hasOperationForField(field: IndexPatternField) {
|
||||
return Boolean(operationSupportMatrix.operationByField[field.name]);
|
||||
return !!operationSupportMatrix.operationByField[field.name];
|
||||
}
|
||||
|
||||
if (!isDraggedField(droppedItem) || !hasOperationForField(droppedItem.field)) {
|
||||
|
@ -176,55 +328,4 @@ const onFieldDrop = ({
|
|||
trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty');
|
||||
setState(mergeLayer({ state, layerId, newLayer }));
|
||||
return true;
|
||||
};
|
||||
|
||||
export function onDrop(props: DatasourceDimensionDropHandlerProps<IndexPatternPrivateState>) {
|
||||
const operationSupportMatrix = getOperationSupportMatrix(props);
|
||||
const { setState, state, droppedItem, columnId, layerId, groupId, isNew } = props;
|
||||
|
||||
if (!isDraggedOperation(droppedItem)) {
|
||||
return onFieldDrop({
|
||||
columnId,
|
||||
setState,
|
||||
state,
|
||||
layerId,
|
||||
droppedItem,
|
||||
operationSupportMatrix,
|
||||
});
|
||||
}
|
||||
const isExistingFromSameGroup =
|
||||
droppedItem.groupId === groupId && droppedItem.columnId !== columnId && !isNew;
|
||||
|
||||
// reorder in the same group
|
||||
if (isExistingFromSameGroup) {
|
||||
return onReorderDrop({
|
||||
columnId,
|
||||
setState,
|
||||
state,
|
||||
layerId,
|
||||
droppedItem,
|
||||
operationSupportMatrix,
|
||||
});
|
||||
}
|
||||
|
||||
// replace or move to compatible group
|
||||
const isFromOtherGroup = droppedItem.groupId !== groupId && droppedItem.layerId === layerId;
|
||||
|
||||
if (isFromOtherGroup) {
|
||||
const layer = state.layers[layerId];
|
||||
const op = { ...layer.columns[droppedItem.columnId] };
|
||||
|
||||
if (props.filterOperations(op)) {
|
||||
return onMoveDropToCompatibleGroup({
|
||||
columnId,
|
||||
setState,
|
||||
state,
|
||||
layerId,
|
||||
droppedItem,
|
||||
operationSupportMatrix,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
.lnsFieldItem {
|
||||
width: 100%;
|
||||
.lnsFieldItem__infoIcon {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
|
@ -13,6 +14,23 @@
|
|||
transition: opacity $euiAnimSpeedFast ease-in-out 1s;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:focus-within,
|
||||
&.kbnFieldButton-isActive {
|
||||
animation: none !important; // sass-lint:disable-line no-important
|
||||
}
|
||||
|
||||
&:focus .kbnFieldButton__name span,
|
||||
&:focus-within .kbnFieldButton__name span,
|
||||
&.kbnFieldButton-isActive .kbnFieldButton__name span {
|
||||
background-color: transparentize($euiColorVis1, .9) !important;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
}
|
||||
|
||||
.kbnFieldButton__name {
|
||||
transition: background-color $euiAnimSpeedFast ease-in-out;
|
||||
}
|
||||
|
||||
.lnsFieldItem--missing {
|
||||
|
|
|
@ -48,11 +48,10 @@ import {
|
|||
} from '../../../../../src/plugins/data/public';
|
||||
import { FieldButton } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
|
||||
import { DraggedField } from './indexpattern';
|
||||
import { DragDrop, DragDropIdentifier } from '../drag_drop';
|
||||
import { DatasourceDataPanelProps, DataType } from '../types';
|
||||
import { BucketedAggregation, FieldStatsResponse } from '../../common';
|
||||
import { IndexPattern, IndexPatternField } from './types';
|
||||
import { IndexPattern, IndexPatternField, DraggedField } from './types';
|
||||
import { LensFieldIcon } from './lens_field_icon';
|
||||
import { trackUiEvent } from '../lens_ui_telemetry';
|
||||
|
||||
|
@ -103,6 +102,8 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
|
|||
dateRange,
|
||||
filters,
|
||||
hideDetails,
|
||||
itemIndex,
|
||||
groupIndex,
|
||||
dropOntoWorkspace,
|
||||
} = props;
|
||||
|
||||
|
@ -167,9 +168,18 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
|
|||
}
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ field, indexPatternId: indexPattern.id, id: field.name } as DraggedField),
|
||||
[field, indexPattern.id]
|
||||
() => ({
|
||||
field,
|
||||
indexPatternId: indexPattern.id,
|
||||
id: field.name,
|
||||
humanData: {
|
||||
label: field.displayName,
|
||||
position: itemIndex + 1,
|
||||
},
|
||||
}),
|
||||
[field, indexPattern.id, itemIndex]
|
||||
);
|
||||
const order = useMemo(() => [0, groupIndex, itemIndex], [groupIndex, itemIndex]);
|
||||
|
||||
const lensFieldIcon = <LensFieldIcon type={field.type as DataType} />;
|
||||
const lensInfoIcon = (
|
||||
|
@ -204,9 +214,8 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
|
|||
container={document.querySelector<HTMLElement>('.application') || undefined}
|
||||
button={
|
||||
<DragDrop
|
||||
noKeyboardSupportYet
|
||||
draggable
|
||||
label={field.displayName}
|
||||
order={order}
|
||||
value={value}
|
||||
dataTestSubj={`lnsFieldListPanelField-${field.name}`}
|
||||
>
|
||||
|
@ -271,6 +280,9 @@ function FieldPanelHeader({
|
|||
indexPatternId,
|
||||
id: field.name,
|
||||
field,
|
||||
humanData: {
|
||||
label: field.displayName,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -641,11 +653,7 @@ const DragToWorkspaceButton = ({
|
|||
dropOntoWorkspace,
|
||||
isEnabled,
|
||||
}: {
|
||||
field: {
|
||||
indexPatternId: string;
|
||||
id: string;
|
||||
field: IndexPatternField;
|
||||
};
|
||||
field: DraggedField;
|
||||
dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace'];
|
||||
isEnabled: boolean;
|
||||
}) => {
|
||||
|
|
|
@ -31,7 +31,7 @@ import { toExpression } from './to_expression';
|
|||
import {
|
||||
IndexPatternDimensionTrigger,
|
||||
IndexPatternDimensionEditor,
|
||||
canHandleDrop,
|
||||
getDropTypes,
|
||||
onDrop,
|
||||
} from './dimension_panel';
|
||||
import { IndexPatternDataPanel } from './datapanel';
|
||||
|
@ -44,7 +44,7 @@ import {
|
|||
import { isDraggedField, normalizeOperationDataType } from './utils';
|
||||
import { LayerPanel } from './layerpanel';
|
||||
import { IndexPatternColumn, getErrorMessages, IncompleteColumn } from './operations';
|
||||
import { IndexPatternField, IndexPatternPrivateState, IndexPatternPersistedState } from './types';
|
||||
import { IndexPatternPrivateState, IndexPatternPersistedState } from './types';
|
||||
import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
|
||||
import { VisualizeFieldContext } from '../../../../../src/plugins/ui_actions/public';
|
||||
|
@ -52,15 +52,9 @@ import { mergeLayer } from './state_helpers';
|
|||
import { Datasource, StateSetter } from '../types';
|
||||
import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
|
||||
import { deleteColumn, isReferenced } from './operations';
|
||||
import { DragDropIdentifier } from '../drag_drop/providers';
|
||||
|
||||
export { OperationType, IndexPatternColumn, deleteColumn } from './operations';
|
||||
|
||||
export type DraggedField = DragDropIdentifier & {
|
||||
field: IndexPatternField;
|
||||
indexPatternId: string;
|
||||
};
|
||||
|
||||
export function columnToOperation(column: IndexPatternColumn, uniqueLabel?: string): Operation {
|
||||
const { dataType, label, isBucketed, scale } = column;
|
||||
return {
|
||||
|
@ -314,8 +308,7 @@ export function getIndexPatternDatasource({
|
|||
domElement
|
||||
);
|
||||
},
|
||||
|
||||
canHandleDrop,
|
||||
getDropTypes,
|
||||
onDrop,
|
||||
|
||||
// Reset the temporary invalid state when closing the editor, but don't
|
||||
|
|
|
@ -253,5 +253,6 @@ export function createMockedDragDropContext(): jest.Mocked<DragContextState> {
|
|||
keyboardMode: false,
|
||||
setKeyboardMode: jest.fn(),
|
||||
setA11yMessage: jest.fn(),
|
||||
registerDropTarget: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { IFieldType } from 'src/plugins/data/common';
|
||||
import { IndexPatternColumn, IncompleteColumn } from './operations';
|
||||
import { IndexPatternAggRestrictions } from '../../../../../src/plugins/data/public';
|
||||
import { DragDropIdentifier } from '../drag_drop/providers';
|
||||
|
||||
export {
|
||||
IndexPatternColumn,
|
||||
|
@ -32,6 +33,10 @@ export {
|
|||
MovingAverageIndexPatternColumn,
|
||||
} from './operations';
|
||||
|
||||
export type DraggedField = DragDropIdentifier & {
|
||||
field: IndexPatternField;
|
||||
indexPatternId: string;
|
||||
};
|
||||
export interface IndexPattern {
|
||||
id: string;
|
||||
fields: IndexPatternField[];
|
||||
|
|
|
@ -6,8 +6,7 @@
|
|||
*/
|
||||
|
||||
import { DataType } from '../types';
|
||||
import { IndexPattern, IndexPatternLayer } from './types';
|
||||
import { DraggedField } from './indexpattern';
|
||||
import { IndexPattern, IndexPatternLayer, DraggedField } from './types';
|
||||
import type {
|
||||
BaseIndexPatternColumn,
|
||||
FieldBasedIndexPatternColumn,
|
||||
|
|
|
@ -138,6 +138,16 @@ export type TableChangeType =
|
|||
| 'reorder'
|
||||
| 'layers';
|
||||
|
||||
export type DropType =
|
||||
| 'field_add'
|
||||
| 'field_replace'
|
||||
| 'reorder'
|
||||
| 'duplicate_in_group'
|
||||
| 'move_compatible'
|
||||
| 'replace_compatible'
|
||||
| 'move_incompatible'
|
||||
| 'replace_incompatible';
|
||||
|
||||
export interface DatasourceSuggestion<T = unknown> {
|
||||
state: T;
|
||||
table: TableSuggestion;
|
||||
|
@ -179,7 +189,9 @@ export interface Datasource<T = unknown, P = unknown> {
|
|||
renderDimensionTrigger: (domElement: Element, props: DatasourceDimensionTriggerProps<T>) => void;
|
||||
renderDimensionEditor: (domElement: Element, props: DatasourceDimensionEditorProps<T>) => void;
|
||||
renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps<T>) => void;
|
||||
canHandleDrop: (props: DatasourceDimensionDropProps<T>) => boolean;
|
||||
getDropTypes: (
|
||||
props: DatasourceDimensionDropProps<T> & { groupId: string }
|
||||
) => DropType | undefined;
|
||||
onDrop: (props: DatasourceDimensionDropHandlerProps<T>) => false | true | { deleted: string };
|
||||
updateStateOnCloseDimension?: (props: {
|
||||
layerId: string;
|
||||
|
@ -299,13 +311,11 @@ export type DatasourceDimensionDropProps<T> = SharedDimensionProps & {
|
|||
state: T;
|
||||
setState: StateSetter<T>;
|
||||
dragDropContext: DragContextState;
|
||||
isReorder?: boolean;
|
||||
};
|
||||
|
||||
export type DatasourceDimensionDropHandlerProps<T> = DatasourceDimensionDropProps<T> & {
|
||||
droppedItem: unknown;
|
||||
groupId: string;
|
||||
isNew?: boolean;
|
||||
dropType: DropType;
|
||||
};
|
||||
|
||||
export type DataType = 'document' | 'string' | 'number' | 'date' | 'boolean' | 'ip';
|
||||
|
|
|
@ -11190,8 +11190,6 @@
|
|||
"xpack.lens.dimensionContainer.close": "閉じる",
|
||||
"xpack.lens.dimensionContainer.closeConfiguration": "構成を閉じる",
|
||||
"xpack.lens.discover.visualizeFieldLegend": "Visualize フィールド",
|
||||
"xpack.lens.dragDrop.elementLifted": "位置 {position} のアイテム {itemLabel} を持ち上げました。",
|
||||
"xpack.lens.dragDrop.elementMoved": "位置 {prevPosition} から位置 {position} までアイテム {itemLabel} を移動しました",
|
||||
"xpack.lens.editLayerSettings": "レイヤー設定を編集",
|
||||
"xpack.lens.editLayerSettingsChartType": "レイヤー設定を編集、{chartType}",
|
||||
"xpack.lens.editorFrame.buildExpressionError": "グラフの準備中に予期しないエラーが発生しました",
|
||||
|
|
|
@ -11219,8 +11219,6 @@
|
|||
"xpack.lens.dimensionContainer.close": "关闭",
|
||||
"xpack.lens.dimensionContainer.closeConfiguration": "关闭配置",
|
||||
"xpack.lens.discover.visualizeFieldLegend": "可视化字段",
|
||||
"xpack.lens.dragDrop.elementLifted": "您已将项目 {itemLabel} 提升到位置 {position}",
|
||||
"xpack.lens.dragDrop.elementMoved": "您已将项目 {itemLabel} 从位置 {prevPosition} 移到位置 {position}",
|
||||
"xpack.lens.editLayerSettings": "编辑图层设置",
|
||||
"xpack.lens.editLayerSettingsChartType": "编辑图层设置 {chartType}",
|
||||
"xpack.lens.editorFrame.buildExpressionError": "准备图表时发生意外错误",
|
||||
|
|
|
@ -53,7 +53,7 @@ export default function ({ getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should reorder the elements for the table', async () => {
|
||||
await PageObjects.lens.reorderDimensions('lnsDatatable_column', 2, 0);
|
||||
await PageObjects.lens.reorderDimensions('lnsDatatable_column', 3, 1);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
expect(await PageObjects.lens.getDimensionTriggersTexts('lnsDatatable_column')).to.eql([
|
||||
'Top values of @message.raw',
|
||||
|
@ -83,6 +83,129 @@ export default function ({ getPageObjects }: FtrProviderContext) {
|
|||
await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel')
|
||||
).to.eql(['Top values of @message.raw']);
|
||||
});
|
||||
|
||||
it('should move the column to non-compatible dimension group', async () => {
|
||||
expect(
|
||||
await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel')
|
||||
).to.eql(['Top values of @message.raw']);
|
||||
|
||||
await PageObjects.lens.dragDimensionToDimension(
|
||||
'lnsXY_splitDimensionPanel > lns-dimensionTrigger',
|
||||
'lnsXY_yDimensionPanel > lns-dimensionTrigger'
|
||||
);
|
||||
|
||||
expect(
|
||||
await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel')
|
||||
).to.eql([]);
|
||||
expect(
|
||||
await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel')
|
||||
).to.eql([]);
|
||||
expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yDimensionPanel')).to.eql([
|
||||
'Unique count of @message.raw',
|
||||
]);
|
||||
});
|
||||
it('should duplicate the column when dragging to empty dimension in the same group', async () => {
|
||||
await PageObjects.lens.dragDimensionToDimension(
|
||||
'lnsXY_yDimensionPanel > lns-dimensionTrigger',
|
||||
'lnsXY_yDimensionPanel > lns-empty-dimension'
|
||||
);
|
||||
await PageObjects.lens.dragDimensionToDimension(
|
||||
'lnsXY_yDimensionPanel > lns-dimensionTrigger',
|
||||
'lnsXY_yDimensionPanel > lns-empty-dimension'
|
||||
);
|
||||
expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yDimensionPanel')).to.eql([
|
||||
'Unique count of @message.raw',
|
||||
'Unique count of @message.raw [1]',
|
||||
'Unique count of @message.raw [2]',
|
||||
]);
|
||||
});
|
||||
it('should duplicate the column when dragging to empty dimension in the same group', async () => {
|
||||
await PageObjects.lens.dragDimensionToDimension(
|
||||
'lnsXY_yDimensionPanel > lns-dimensionTrigger',
|
||||
'lnsXY_xDimensionPanel > lns-empty-dimension'
|
||||
);
|
||||
expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yDimensionPanel')).to.eql([
|
||||
'Unique count of @message.raw',
|
||||
'Unique count of @message.raw [1]',
|
||||
]);
|
||||
expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql([
|
||||
'Top values of @message.raw',
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('keyboard drag and drop', () => {
|
||||
it('should drop a field to workspace', async () => {
|
||||
await PageObjects.visualize.navigateToNewVisualization();
|
||||
await PageObjects.visualize.clickVisType('lens');
|
||||
await PageObjects.lens.goToTimeRange();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.lens.dragFieldWithKeyboard('@timestamp');
|
||||
expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel')).to.eql(
|
||||
'@timestamp'
|
||||
);
|
||||
});
|
||||
it('should drop a field to empty dimension', async () => {
|
||||
await PageObjects.lens.dragFieldWithKeyboard('bytes', 4);
|
||||
expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yDimensionPanel')).to.eql([
|
||||
'Count of records',
|
||||
'Average of bytes',
|
||||
]);
|
||||
await PageObjects.lens.dragFieldWithKeyboard('@message.raw', 1, true);
|
||||
expect(
|
||||
await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel')
|
||||
).to.eql(['Top values of @message.raw']);
|
||||
});
|
||||
it('should drop a field to an existing dimension replacing the old one', async () => {
|
||||
await PageObjects.lens.dragFieldWithKeyboard('clientip', 1, true);
|
||||
expect(
|
||||
await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel')
|
||||
).to.eql(['Top values of clientip']);
|
||||
});
|
||||
it('should duplicate an element in a group', async () => {
|
||||
await PageObjects.lens.dimensionKeyboardDragDrop('lnsXY_yDimensionPanel', 0, 1);
|
||||
expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yDimensionPanel')).to.eql([
|
||||
'Count of records',
|
||||
'Average of bytes',
|
||||
'Count of records [1]',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should move dimension to compatible dimension', async () => {
|
||||
await PageObjects.lens.dimensionKeyboardDragDrop('lnsXY_xDimensionPanel', 0, 5);
|
||||
expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql(
|
||||
[]
|
||||
);
|
||||
expect(
|
||||
await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel')
|
||||
).to.eql(['@timestamp']);
|
||||
|
||||
await PageObjects.lens.dimensionKeyboardDragDrop('lnsXY_splitDimensionPanel', 0, 5, true);
|
||||
expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql([
|
||||
'@timestamp',
|
||||
]);
|
||||
expect(
|
||||
await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel')
|
||||
).to.eql([]);
|
||||
});
|
||||
it('should move dimension to incompatible dimension', async () => {
|
||||
await PageObjects.lens.dimensionKeyboardDragDrop('lnsXY_yDimensionPanel', 1, 2);
|
||||
expect(
|
||||
await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel')
|
||||
).to.eql(['bytes']);
|
||||
|
||||
await PageObjects.lens.dimensionKeyboardDragDrop('lnsXY_xDimensionPanel', 0, 2);
|
||||
expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yDimensionPanel')).to.eql([
|
||||
'Count of records',
|
||||
'Unique count of @timestamp',
|
||||
]);
|
||||
});
|
||||
it('should reorder elements with keyboard', async () => {
|
||||
await PageObjects.lens.dimensionKeyboardReorder('lnsXY_yDimensionPanel', 0, 1);
|
||||
expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yDimensionPanel')).to.eql([
|
||||
'Unique count of @timestamp',
|
||||
'Count of records',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('workspace drop', () => {
|
||||
|
|
|
@ -163,6 +163,73 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
},
|
||||
|
||||
/**
|
||||
* Copies field to chosen destination that is defined by distance of `steps`
|
||||
* (right arrow presses) from it
|
||||
*
|
||||
* @param fieldName - the desired field for the dimension
|
||||
* @param steps - number of steps user has to press right
|
||||
* @param reverse - defines the direction of going through drops
|
||||
* */
|
||||
async dragFieldWithKeyboard(fieldName: string, steps = 1, reverse = false) {
|
||||
const field = await find.byCssSelector(
|
||||
`[data-test-subj="lnsDragDrop_draggable-${fieldName}"] [data-test-subj="lnsDragDrop-keyboardHandler"]`
|
||||
);
|
||||
await field.focus();
|
||||
await browser.pressKeys(browser.keys.ENTER);
|
||||
for (let i = 0; i < steps; i++) {
|
||||
await browser.pressKeys(reverse ? browser.keys.LEFT : browser.keys.RIGHT);
|
||||
}
|
||||
await browser.pressKeys(browser.keys.ENTER);
|
||||
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
},
|
||||
|
||||
/**
|
||||
* Selects draggable element and moves it by number of `steps`
|
||||
*
|
||||
* @param group - the group of the element
|
||||
* @param index - the index of the element in the group
|
||||
* @param steps - number of steps of presses right or left
|
||||
* @param reverse - defines the direction of going through drops
|
||||
* */
|
||||
async dimensionKeyboardDragDrop(group: string, index = 0, steps = 1, reverse = false) {
|
||||
const elements = await find.allByCssSelector(
|
||||
`[data-test-subj="${group}"] [data-test-subj="lnsDragDrop-keyboardHandler"]`
|
||||
);
|
||||
const el = elements[index];
|
||||
await el.focus();
|
||||
await browser.pressKeys(browser.keys.ENTER);
|
||||
for (let i = 0; i < steps; i++) {
|
||||
await browser.pressKeys(reverse ? browser.keys.LEFT : browser.keys.RIGHT);
|
||||
}
|
||||
await browser.pressKeys(browser.keys.ENTER);
|
||||
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
},
|
||||
/**
|
||||
* Selects draggable element and reorders it by number of `steps`
|
||||
*
|
||||
* @param group - the group of the element
|
||||
* @param index - the index of the element in the group
|
||||
* @param steps - number of steps of presses right or left
|
||||
* @param reverse - defines the direction of going through drops
|
||||
* */
|
||||
async dimensionKeyboardReorder(group: string, index = 0, steps = 1, reverse = false) {
|
||||
const elements = await find.allByCssSelector(
|
||||
`[data-test-subj="${group}"] [data-test-subj="lnsDragDrop-keyboardHandler"]`
|
||||
);
|
||||
const el = elements[index];
|
||||
await el.focus();
|
||||
await browser.pressKeys(browser.keys.ENTER);
|
||||
for (let i = 0; i < steps; i++) {
|
||||
await browser.pressKeys(reverse ? browser.keys.ARROW_UP : browser.keys.ARROW_DOWN);
|
||||
}
|
||||
await browser.pressKeys(browser.keys.ENTER);
|
||||
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
},
|
||||
|
||||
/**
|
||||
* Drags field to dimension trigger
|
||||
*
|
||||
|
@ -194,16 +261,12 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
/**
|
||||
* Reorder elements within the group
|
||||
*
|
||||
* @param startIndex - the index of dragging element
|
||||
* @param endIndex - the index of drop
|
||||
* @param startIndex - the index of dragging element starting from 1
|
||||
* @param endIndex - the index of drop starting from 1
|
||||
* */
|
||||
async reorderDimensions(dimension: string, startIndex: number, endIndex: number) {
|
||||
const dragging = `[data-test-subj='${dimension}']:nth-of-type(${
|
||||
startIndex + 1
|
||||
}) .lnsDragDrop`;
|
||||
const dropping = `[data-test-subj='${dimension}']:nth-of-type(${
|
||||
endIndex + 1
|
||||
}) [data-test-subj='lnsDragDrop-reorderableDropLayer'`;
|
||||
const dragging = `[data-test-subj='${dimension}']:nth-of-type(${startIndex}) .lnsDragDrop`;
|
||||
const dropping = `[data-test-subj='${dimension}']:nth-of-type(${endIndex}) [data-test-subj='lnsDragDrop-reorderableDropLayer'`;
|
||||
await browser.html5DragAndDrop(dragging, dropping);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue