[Lens] Drag and drop accessibility messages (#91494)

* [Lens] copy dnd

* Update x-pack/plugins/lens/public/drag_drop/providers.tsx

Co-authored-by: Wylie Conlon <wylieconlon@gmail.com>

Co-authored-by: Wylie Conlon <wylieconlon@gmail.com>
This commit is contained in:
Marta Bondyra 2021-02-17 16:48:26 +01:00 committed by GitHub
parent 31cbfbb8d5
commit 351990068d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 249 additions and 112 deletions

View file

@ -11,6 +11,7 @@ export interface HumanData {
label: string;
groupLabel?: string;
position?: number;
nextLabel?: string;
}
type AnnouncementFunction = (draggedElement: HumanData, dropElement: HumanData) => string;
@ -25,7 +26,7 @@ const selectedTargetReplace = (
{ 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}.`,
defaultMessage: `Replace {dropLabel} in {groupLabel} group at position {position} with {label}. Press space or enter to replace`,
values: {
label,
dropLabel,
@ -34,65 +35,103 @@ const selectedTargetReplace = (
},
});
const droppedReplace = ({ label }: HumanData, { label: dropLabel, groupLabel }: HumanData) =>
const droppedReplace = (
{ label }: HumanData,
{ label: dropLabel, groupLabel, position }: HumanData
) =>
i18n.translate('xpack.lens.dragDrop.announce.duplicated.replace', {
defaultMessage:
'You have dropped the item. You have replaced {dropLabel} with {label} in {groupLabel} group.',
defaultMessage: 'Replaced {dropLabel} with {label} in {groupLabel} at position {position}',
values: {
label,
dropLabel,
groupLabel,
position,
},
});
export const announcements: CustomAnnouncementsType = {
selectedTarget: {
reorder: ({ label, position: prevPosition }, { position }) =>
reorder: ({ label, groupLabel, position: prevPosition }, { position }) =>
prevPosition === position
? i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.reorderedBack', {
defaultMessage: `You have moved the item {label} back to position {prevPosition}`,
defaultMessage: `{label} returned to its initial 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}`,
defaultMessage: `Reorder {label} in {groupLabel} group from position {prevPosition} to position {position}. Press space or enter to reorder`,
values: {
groupLabel,
label,
position,
prevPosition,
},
}),
duplicate_in_group: ({ label }, { label: dropLabel, groupLabel, position }) =>
duplicate_in_group: ({ label }, { 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}.`,
defaultMessage: `Duplicate {label} to {groupLabel} group at position {position}. Press space or enter to duplicate`,
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}',
replace_incompatible: (
{ label }: HumanData,
{ label: dropLabel, groupLabel, position, nextLabel }: HumanData
) =>
i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.replaceIncompatible', {
defaultMessage: `Convert {label} to {nextLabel} and replace {dropLabel} in {groupLabel} group at position {position}. Press space or enter to replace`,
values: {
label,
nextLabel,
dropLabel,
groupLabel,
position,
},
}),
move_incompatible: (
{ label }: HumanData,
{ label: groupLabel, position, nextLabel }: HumanData
) =>
i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.moveIncompatible', {
defaultMessage: `Convert {label} to {nextLabel} and move to {groupLabel} group at position {position}. Press space or enter to move`,
values: {
label,
nextLabel,
groupLabel,
position,
},
}),
move_compatible: ({ label }: HumanData, { groupLabel, position }: HumanData) =>
i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.moveCompatible', {
defaultMessage: `Move {label} to {groupLabel} group at position {position}. Press space or enter to move`,
values: {
label,
groupLabel,
position,
},
}),
},
dropped: {
reorder: ({ label, groupLabel, position: prevPosition }, { position }) =>
i18n.translate('xpack.lens.dragDrop.announce.dropped.reordered', {
defaultMessage:
'Reordered {label} in {groupLabel} group from position {prevPosition} to positon {position}',
values: {
label,
groupLabel,
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}',
defaultMessage: 'Duplicated {label} in {groupLabel} group at position {position}',
values: {
label,
groupLabel,
@ -101,7 +140,42 @@ export const announcements: CustomAnnouncementsType = {
}),
field_replace: droppedReplace,
replace_compatible: droppedReplace,
replace_incompatible: droppedReplace,
replace_incompatible: (
{ label }: HumanData,
{ label: dropLabel, groupLabel, position, nextLabel }: HumanData
) =>
i18n.translate('xpack.lens.dragDrop.announce.dropped.replaceIncompatible', {
defaultMessage:
'Converted {label} to {nextLabel} and replaced {dropLabel} in {groupLabel} group at position {position}',
values: {
label,
nextLabel,
dropLabel,
groupLabel,
position,
},
}),
move_incompatible: ({ label }: HumanData, { groupLabel, position, nextLabel }: HumanData) =>
i18n.translate('xpack.lens.dragDrop.announce.dropped.moveIncompatible', {
defaultMessage:
'Converted {label} to {nextLabel} and moved to {groupLabel} group at position {position}',
values: {
label,
nextLabel,
groupLabel,
position,
},
}),
move_compatible: ({ label }: HumanData, { groupLabel, position }: HumanData) =>
i18n.translate('xpack.lens.dragDrop.announce.dropped.moveCompatible', {
defaultMessage: 'Moved {label} to {groupLabel} group at position {position}',
values: {
label,
groupLabel,
position,
},
}),
},
};
@ -113,13 +187,29 @@ const defaultAnnouncements = {
label,
},
}),
cancelled: () =>
i18n.translate('xpack.lens.dragDrop.announce.cancelled', {
defaultMessage: 'Movement cancelled',
}),
cancelled: ({ label, groupLabel, position }: HumanData) => {
if (!groupLabel || !position) {
return i18n.translate('xpack.lens.dragDrop.announce.cancelled', {
defaultMessage: 'Movement cancelled. {label} returned to its initial position',
values: {
label,
},
});
}
return i18n.translate('xpack.lens.dragDrop.announce.cancelledItem', {
defaultMessage:
'Movement cancelled. {label} returned to {groupLabel} group at position {position}',
values: {
label,
groupLabel,
position,
},
});
},
noTarget: () => {
return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.noSelected', {
defaultMessage: `No target selected. Use arrow keys to select a target.`,
defaultMessage: `No target selected. Use arrow keys to select a target`,
});
},
@ -129,17 +219,15 @@ const defaultAnnouncements = {
) =>
dropGroupLabel && position
? i18n.translate('xpack.lens.dragDrop.announce.droppedDefault', {
defaultMessage:
'You have dropped {label} to {dropLabel} in {dropGroupLabel} group at position {position}',
defaultMessage: 'Added {label} 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}',
defaultMessage: 'Added {label} to {dropLabel}',
values: {
label,
dropLabel,
@ -151,16 +239,15 @@ const defaultAnnouncements = {
) => {
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}`,
defaultMessage: `Add {label} to {dropGroupLabel} group at position {position}. Press space or enter to add`,
values: {
dropLabel,
label,
dropGroupLabel,
position,
},
})
: i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.defaultNoPosition', {
defaultMessage: `Selected {dropLabel}. Press space or enter to drop {label}`,
defaultMessage: `Add {label} to {dropLabel}. Press space or enter to add`,
values: {
dropLabel,
label,

View file

@ -110,7 +110,7 @@ describe('DragDrop', () => {
const component = mount(
<ChildDragDropProvider
{...defaultContext}
dragging={{ id: '2', humanData: { label: 'label1' } }}
dragging={{ id: '2', humanData: { label: 'Label1' } }}
setDragging={setDragging}
>
<DragDrop onDrop={onDrop} dropType="field_add" value={value} order={[2, 0, 1, 0]}>
@ -126,7 +126,7 @@ describe('DragDrop', () => {
expect(preventDefault).toBeCalled();
expect(stopPropagation).toBeCalled();
expect(setDragging).toBeCalledWith(undefined);
expect(onDrop).toBeCalledWith({ id: '2', humanData: { label: 'label1' } }, 'field_add');
expect(onDrop).toBeCalledWith({ id: '2', humanData: { label: 'Label1' } }, 'field_add');
});
test('drop function is not called on dropType undefined', async () => {
@ -138,7 +138,7 @@ describe('DragDrop', () => {
const component = mount(
<ChildDragDropProvider
{...defaultContext}
dragging={{ id: 'hi', humanData: { label: 'label1' } }}
dragging={{ id: 'hi', humanData: { label: 'Label1' } }}
setDragging={setDragging}
>
<DragDrop onDrop={onDrop} dropType={undefined} value={value} order={[2, 0, 1, 0]}>
@ -195,7 +195,7 @@ describe('DragDrop', () => {
});
test('additional styles are reflected in the className until drop', () => {
let dragging: { id: '1'; humanData: { label: 'label1' } } | undefined;
let dragging: { id: '1'; humanData: { label: 'Label1' } } | undefined;
const getAdditionalClassesOnEnter = jest.fn().mockReturnValue('additional');
const getAdditionalClassesOnDroppable = jest.fn().mockReturnValue('droppable');
const setA11yMessage = jest.fn();
@ -206,7 +206,7 @@ describe('DragDrop', () => {
dragging={dragging}
setA11yMessage={setA11yMessage}
setDragging={() => {
dragging = { id: '1', humanData: { label: 'label1' } };
dragging = { id: '1', humanData: { label: 'Label1' } };
}}
>
<DragDrop
@ -242,7 +242,7 @@ describe('DragDrop', () => {
});
test('additional enter styles are reflected in the className until dragleave', () => {
let dragging: { id: '1'; humanData: { label: 'label1' } } | 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();
@ -252,7 +252,7 @@ describe('DragDrop', () => {
setA11yMessage={jest.fn()}
dragging={dragging}
setDragging={() => {
dragging = { id: '1', humanData: { label: 'label1' } };
dragging = { id: '1', humanData: { label: 'Label1' } };
}}
setActiveDropTarget={setActiveDropTarget}
activeDropTarget={
@ -303,7 +303,7 @@ describe('DragDrop', () => {
draggable: true,
value: {
id: '1',
humanData: { label: 'label1', position: 1 },
humanData: { label: 'Label1', position: 1 },
},
children: '1',
order: [2, 0, 0, 0],
@ -326,7 +326,7 @@ describe('DragDrop', () => {
dragType: 'move' as 'copy' | 'move',
value: {
id: '3',
humanData: { label: 'label3', position: 1 },
humanData: { label: 'label3', position: 1, groupLabel: 'Y' },
},
onDrop,
dropType: 'replace_compatible' as DropType,
@ -337,7 +337,7 @@ describe('DragDrop', () => {
dragType: 'move' as 'copy' | 'move',
value: {
id: '4',
humanData: { label: 'label4', position: 2 },
humanData: { label: 'label4', position: 2, groupLabel: 'Y' },
},
order: [2, 0, 2, 1],
},
@ -380,11 +380,11 @@ describe('DragDrop', () => {
});
keyboardHandler.simulate('keydown', { key: 'Enter' });
expect(setA11yMessage).toBeCalledWith(
'Selected label3 in group at position 1. Press space or enter to replace label3 with label1.'
'Replace label3 in Y group at position 1 with Label1. Press space or enter to replace'
);
expect(setActiveDropTarget).toBeCalledWith(undefined);
expect(onDrop).toBeCalledWith(
{ humanData: { label: 'label1', position: 1 }, id: '1' },
{ humanData: { label: 'Label1', position: 1 }, id: '1' },
'move_compatible'
);
});
@ -437,7 +437,7 @@ describe('DragDrop', () => {
draggable: true,
value: {
id: '1',
humanData: { label: 'label1', position: 1 },
humanData: { label: 'Label1', position: 1 },
},
children: '1',
order: [2, 0, 0, 0],
@ -488,19 +488,19 @@ describe('DragDrop', () => {
const items = [
{
id: '1',
humanData: { label: 'label1', position: 1 },
humanData: { label: 'Label1', position: 1, groupLabel: 'X' },
onDrop,
dropType: 'reorder' as DropType,
},
{
id: '2',
humanData: { label: 'label2', position: 2 },
humanData: { label: 'label2', position: 2, groupLabel: 'X' },
onDrop,
dropType: 'reorder' as DropType,
},
{
id: '3',
humanData: { label: 'label3', position: 3 },
humanData: { label: 'label3', position: 3, groupLabel: 'X' },
onDrop,
dropType: 'reorder' as DropType,
},
@ -583,7 +583,7 @@ describe('DragDrop', () => {
});
expect(setDragging).toBeCalledWith({ ...items[0] });
expect(setA11yMessage).toBeCalledWith('Lifted label1');
expect(setA11yMessage).toBeCalledWith('Lifted Label1');
expect(
component
.find('[data-test-subj="lnsDragDrop-reorderableGroup"]')
@ -652,7 +652,7 @@ describe('DragDrop', () => {
jest.runAllTimers();
expect(setA11yMessage).toBeCalledWith(
'You have dropped the item label1. You have moved the item from position 1 to positon 3'
'Reordered Label1 in X group from position 1 to positon 3'
);
expect(preventDefault).toBeCalled();
expect(stopPropagation).toBeCalled();
@ -687,7 +687,7 @@ describe('DragDrop', () => {
expect(setActiveDropTarget).toBeCalledWith(items[1]);
expect(setA11yMessage).toBeCalledWith(
'You have moved the item label1 from position 1 to position 2'
'Reorder Label1 in X group from position 1 to position 2. Press space or enter to reorder'
);
});
test(`Keyboard navigation: user can drop element to an activeDropTarget`, () => {
@ -729,13 +729,17 @@ describe('DragDrop', () => {
jest.runAllTimers();
expect(onDropHandler).not.toHaveBeenCalled();
expect(setA11yMessage).toBeCalledWith('Movement cancelled');
expect(setA11yMessage).toBeCalledWith(
'Movement cancelled. Label1 returned to X group at position 1'
);
keyboardHandler.simulate('keydown', { key: 'Space' });
keyboardHandler.simulate('keydown', { key: 'ArrowDown' });
keyboardHandler.simulate('blur');
expect(onDropHandler).not.toHaveBeenCalled();
expect(setA11yMessage).toBeCalledWith('Movement cancelled');
expect(setA11yMessage).toBeCalledWith(
'Movement cancelled. Label1 returned to X group at position 1'
);
});
test(`Keyboard Navigation: Reordered elements get extra styles to show the reorder effect`, () => {
@ -772,7 +776,7 @@ describe('DragDrop', () => {
component.find('[data-test-subj="lnsDragDrop-translatableDrop"]').at(1).prop('style')
).toEqual(undefined);
expect(setA11yMessage).toBeCalledWith(
'You have moved the item label1 from position 1 to position 2'
'Reorder Label1 in X group from position 1 to position 2. Press space or enter to reorder'
);
component
@ -837,7 +841,7 @@ describe('DragDrop', () => {
keyboardHandler.simulate('keydown', { key: 'Space' });
keyboardHandler.simulate('keydown', { key: 'ArrowUp' });
expect(setActiveDropTarget).toBeCalledWith(undefined);
expect(setA11yMessage).toBeCalledWith('You have moved the item label1 back to position 1');
expect(setA11yMessage).toBeCalledWith('Label1 returned to its initial position 1');
});
});
});

View file

@ -267,7 +267,7 @@ const DragInner = memo(function DragInner({
setDragging(undefined);
setActiveDropTarget(undefined);
setKeyboardMode(false);
setA11yMessage(announce.cancelled());
setA11yMessage(announce.cancelled(value.humanData));
if (onDragEnd) {
onDragEnd();
}

View file

@ -204,12 +204,12 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode }
</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.`,
defaultMessage: `Press space or enter to start 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 space or enter again to finish.`,
})}
</p>
<p id={`lnsDragDrop-keyboardInstructions`}>
{i18n.translate('xpack.lens.dragDrop.keyboardInstructions', {
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.`,
defaultMessage: `Press space or enter to start dragging. When dragging, use the left/right arrow keys to move between drop targets. Press space or enter again to finish.`,
})}
</p>
</div>

View file

@ -56,7 +56,7 @@ const { dragging } = useContext(DragContext);
return (
<DragDrop
className="axis"
dropType={getDropTypes(dragging)}
dropType={getDropProps(dragging)}
onDrop={(item) => onChange([...items, item])}
>
{items.map((x) => (

View file

@ -64,13 +64,16 @@ export function DraggableDimensionButton({
columnId: string;
registerNewButtonRef: (id: string, instance: HTMLDivElement | null) => void;
}) {
const dropType = layerDatasource.getDropTypes({
const dropProps = layerDatasource.getDropProps({
...layerDatasourceDropProps,
columnId,
filterOperations: group.filterOperations,
groupId: group.groupId,
});
const dropType = dropProps?.dropType;
const nextLabel = dropProps?.nextLabel;
const value = useMemo(
() => ({
columnId,
@ -82,9 +85,10 @@ export function DraggableDimensionButton({
label,
groupLabel: group.groupLabel,
position: accessorIndex + 1,
nextLabel: nextLabel || '',
},
}),
[columnId, group.groupId, accessorIndex, layerId, dropType, label, group.groupLabel]
[columnId, group.groupId, accessorIndex, layerId, dropType, label, group.groupLabel, nextLabel]
);
// todo: simplify by id and use drop targets?

View file

@ -54,13 +54,16 @@ export function EmptyDimensionButton({
setNewColumnId(generateId());
}, [itemIndex]);
const dropType = layerDatasource.getDropTypes({
const dropProps = layerDatasource.getDropProps({
...layerDatasourceDropProps,
columnId: newColumnId,
filterOperations: group.filterOperations,
groupId: group.groupId,
});
const dropType = dropProps?.dropType;
const nextLabel = dropProps?.nextLabel;
const value = useMemo(
() => ({
columnId: newColumnId,
@ -72,9 +75,10 @@ export function EmptyDimensionButton({
label,
groupLabel: group.groupLabel,
position: itemIndex + 1,
nextLabel: nextLabel || '',
},
}),
[dropType, newColumnId, group.groupId, layerId, group.groupLabel, itemIndex]
[dropType, newColumnId, group.groupId, layerId, group.groupLabel, itemIndex, nextLabel]
);
return (

View file

@ -440,7 +440,10 @@ describe('LayerPanel', () => {
],
});
mockDatasource.getDropTypes.mockReturnValue('field_add');
mockDatasource.getDropProps.mockReturnValue({
dropType: 'field_add',
nextLabel: '',
});
const draggingField = {
field: { name: 'dragged' },
@ -459,7 +462,7 @@ describe('LayerPanel', () => {
</ChildDragDropProvider>
);
expect(mockDatasource.getDropTypes).toHaveBeenCalledWith(
expect(mockDatasource.getDropProps).toHaveBeenCalledWith(
expect.objectContaining({
dragDropContext: expect.objectContaining({
dragging: draggingField,
@ -492,8 +495,8 @@ describe('LayerPanel', () => {
],
});
mockDatasource.getDropTypes.mockImplementation(({ columnId }) =>
columnId !== 'a' ? 'field_replace' : undefined
mockDatasource.getDropProps.mockImplementation(({ columnId }) =>
columnId !== 'a' ? { dropType: 'field_replace', nextLabel: '' } : undefined
);
const draggingField = {
@ -513,7 +516,7 @@ describe('LayerPanel', () => {
</ChildDragDropProvider>
);
expect(mockDatasource.getDropTypes).toHaveBeenCalledWith(
expect(mockDatasource.getDropProps).toHaveBeenCalledWith(
expect.objectContaining({ columnId: 'a' })
);
@ -554,7 +557,10 @@ describe('LayerPanel', () => {
],
});
mockDatasource.getDropTypes.mockReturnValue('replace_compatible');
mockDatasource.getDropProps.mockReturnValue({
dropType: 'replace_compatible',
nextLabel: '',
});
const draggingOperation = {
layerId: 'first',
@ -574,7 +580,7 @@ describe('LayerPanel', () => {
</ChildDragDropProvider>
);
expect(mockDatasource.getDropTypes).toHaveBeenCalledWith(
expect(mockDatasource.getDropProps).toHaveBeenCalledWith(
expect.objectContaining({
dragDropContext: expect.objectContaining({
dragging: draggingOperation,

View file

@ -88,7 +88,7 @@ export function createMockDatasource(id: string): DatasourceMock {
uniqueLabels: jest.fn((_state) => ({})),
renderDimensionTrigger: jest.fn(),
renderDimensionEditor: jest.fn(),
getDropTypes: jest.fn(),
getDropProps: jest.fn(),
onDrop: jest.fn(),
// this is an additional property which doesn't exist on real datasources

View file

@ -6,7 +6,7 @@
*/
import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public';
import { IndexPatternDimensionEditorProps } from './dimension_panel';
import { onDrop, getDropTypes } from './droppable';
import { onDrop, getDropProps } from './droppable';
import { DragContextState } from '../../drag_drop';
import { createMockedDragDropContext } from '../mocks';
import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup, CoreSetup } from 'kibana/public';
@ -91,7 +91,7 @@ const draggingField = {
* - Dimension trigger: Not tested here
* - Dimension editor component: First half of the tests
*
* - getDropTypes: Returns drop types that are possible for the current dragging field or other dimension
* - getDropProps: Returns drop types that are possible for the current dragging field or other dimension
* - onDrop: Correct application of drop logic
*/
describe('IndexPatternDimensionEditorPanel', () => {
@ -174,14 +174,14 @@ describe('IndexPatternDimensionEditorPanel', () => {
});
const groupId = 'a';
describe('getDropTypes', () => {
describe('getDropProps', () => {
it('returns undefined if no drag is happening', () => {
expect(getDropTypes({ ...defaultProps, groupId, dragDropContext })).toBe(undefined);
expect(getDropProps({ ...defaultProps, groupId, dragDropContext })).toBe(undefined);
});
it('returns undefined if the dragged item has no field', () => {
expect(
getDropTypes({
getDropProps({
...defaultProps,
groupId,
dragDropContext: {
@ -198,7 +198,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
it('returns undefined if field is not supported by filterOperations', () => {
expect(
getDropTypes({
getDropProps({
...defaultProps,
groupId,
dragDropContext: {
@ -217,7 +217,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
it('returns remove_add if the field is supported by filterOperations and the dropTarget is an existing column', () => {
expect(
getDropTypes({
getDropProps({
...defaultProps,
groupId,
dragDropContext: {
@ -226,12 +226,12 @@ describe('IndexPatternDimensionEditorPanel', () => {
},
filterOperations: (op: OperationMetadata) => op.dataType === 'number',
})
).toBe('field_replace');
).toEqual({ dropType: 'field_replace', nextLabel: 'Intervals' });
});
it('returns undefined if the field belongs to another index pattern', () => {
expect(
getDropTypes({
getDropProps({
...defaultProps,
groupId,
dragDropContext: {
@ -250,7 +250,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
it('returns undefined if the dragged field is already in use by this operation', () => {
expect(
getDropTypes({
getDropProps({
...defaultProps,
groupId,
dragDropContext: {
@ -275,7 +275,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
it('returns move if the dragged column is compatible', () => {
expect(
getDropTypes({
getDropProps({
...defaultProps,
groupId,
dragDropContext: {
@ -290,7 +290,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
},
columnId: 'col2',
})
).toBe('move_compatible');
).toEqual({ dropType: 'move_compatible' });
});
it('returns undefined if the dragged column from different group uses the same field as the dropTarget', () => {
@ -318,7 +318,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
};
expect(
getDropTypes({
getDropProps({
...defaultProps,
groupId,
dragDropContext: {
@ -357,7 +357,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
};
expect(
getDropTypes({
getDropProps({
...defaultProps,
groupId,
dragDropContext: {
@ -373,7 +373,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
columnId: 'col2',
filterOperations: (op: OperationMetadata) => op.isBucketed === false,
})
).toEqual('replace_incompatible');
).toEqual({ dropType: 'replace_incompatible', nextLabel: 'Unique count' });
});
});
describe('onDrop', () => {

View file

@ -10,21 +10,29 @@ import {
DatasourceDimensionDropHandlerProps,
isDraggedOperation,
DraggedOperation,
DropType,
} from '../../types';
import { IndexPatternColumn } from '../indexpattern';
import { insertOrReplaceColumn, deleteColumn, getOperationTypesForField } from '../operations';
import {
insertOrReplaceColumn,
deleteColumn,
getOperationTypesForField,
getOperationDisplay,
} from '../operations';
import { mergeLayer } from '../state_helpers';
import { hasField, isDraggedField } from '../utils';
import { IndexPatternPrivateState, IndexPatternField, DraggedField } from '../types';
import { IndexPatternPrivateState, DraggedField } from '../types';
import { trackUiEvent } from '../../lens_ui_telemetry';
type DropHandlerProps<T> = DatasourceDimensionDropHandlerProps<IndexPatternPrivateState> & {
droppedItem: T;
};
export function getDropTypes(
const operationLabels = getOperationDisplay();
export function getDropProps(
props: DatasourceDimensionDropProps<IndexPatternPrivateState> & { groupId: string }
) {
): { dropType: DropType; nextLabel?: string } | undefined {
const { dragging } = props.dragDropContext;
if (!dragging) {
return;
@ -32,23 +40,27 @@ export function getDropTypes(
const layerIndexPatternId = props.state.layers[props.layerId].indexPatternId;
function hasOperationForField(field: IndexPatternField) {
const operationsForNewField = getOperationTypesForField(field, props.filterOperations);
return !!operationsForNewField.length;
}
const currentColumn = props.state.layers[props.layerId].columns[props.columnId];
if (isDraggedField(dragging)) {
if (
!!(layerIndexPatternId === dragging.indexPatternId && hasOperationForField(dragging.field))
) {
const operationsForNewField = getOperationTypesForField(dragging.field, props.filterOperations);
if (!!(layerIndexPatternId === dragging.indexPatternId && operationsForNewField.length)) {
const highestPriorityOperationLabel = operationLabels[operationsForNewField[0]].displayName;
if (!currentColumn) {
return 'field_add';
return { dropType: 'field_add', nextLabel: highestPriorityOperationLabel };
} else if (
(hasField(currentColumn) && currentColumn.sourceField !== dragging.field.name) ||
!hasField(currentColumn)
) {
return 'field_replace';
const persistingOperationLabel =
currentColumn &&
operationsForNewField.includes(currentColumn.operationType) &&
operationLabels[currentColumn.operationType].displayName;
return {
dropType: 'field_replace',
nextLabel: persistingOperationLabel || highestPriorityOperationLabel,
};
}
}
return;
@ -62,9 +74,9 @@ export function getDropTypes(
// same group
if (props.groupId === dragging.groupId) {
if (currentColumn) {
return 'reorder';
return { dropType: 'reorder' };
}
return 'duplicate_in_group';
return { dropType: 'duplicate_in_group' };
}
// compatible group
@ -80,20 +92,34 @@ export function getDropTypes(
}
if (props.filterOperations(op)) {
if (currentColumn) {
return 'replace_compatible'; // in the future also 'swap_compatible' and 'duplicate_compatible'
return { dropType: 'replace_compatible' }; // in the future also 'swap_compatible' and 'duplicate_compatible'
} else {
return 'move_compatible'; // in the future also 'duplicate_compatible'
return { dropType: 'move_compatible' }; // in the future also 'duplicate_compatible'
}
}
// suggest
const field =
hasField(op) && props.state.indexPatterns[layerIndexPatternId].getFieldByName(op.sourceField);
if (field && hasOperationForField(field)) {
const operationsForNewField = field && getOperationTypesForField(field, props.filterOperations);
if (operationsForNewField && operationsForNewField?.length) {
const highestPriorityOperationLabel = operationLabels[operationsForNewField[0]].displayName;
if (currentColumn) {
return 'replace_incompatible'; // in the future also 'swap_incompatible', 'duplicate_incompatible'
const persistingOperationLabel =
currentColumn &&
operationsForNewField.includes(currentColumn.operationType) &&
operationLabels[currentColumn.operationType].displayName;
return {
dropType: 'replace_incompatible',
nextLabel: persistingOperationLabel || highestPriorityOperationLabel,
}; // in the future also 'swap_incompatible', 'duplicate_incompatible'
} else {
return 'move_incompatible'; // in the future also 'duplicate_incompatible'
return {
dropType: 'move_incompatible',
nextLabel: highestPriorityOperationLabel,
}; // in the future also 'duplicate_incompatible'
}
}
}
@ -178,6 +204,12 @@ function onMoveDropToNonCompatibleGroup(props: DropHandlerProps<DraggedOperation
}
const currentIndexPattern = state.indexPatterns[layer.indexPatternId];
// Detects if we can change the field only, otherwise change field + operation
const selectedColumn: IndexPatternColumn | null = layer.columns[columnId] || null;
const fieldIsCompatibleWithCurrent =
selectedColumn && operationsForNewField.includes(selectedColumn.operationType);
const newLayer = insertOrReplaceColumn({
layer: deleteColumn({
@ -187,7 +219,7 @@ function onMoveDropToNonCompatibleGroup(props: DropHandlerProps<DraggedOperation
}),
columnId,
indexPattern: currentIndexPattern,
op: operationsForNewField[0],
op: fieldIsCompatibleWithCurrent ? selectedColumn.operationType : operationsForNewField[0],
field,
});

View file

@ -31,7 +31,7 @@ import { toExpression } from './to_expression';
import {
IndexPatternDimensionTrigger,
IndexPatternDimensionEditor,
getDropTypes,
getDropProps,
onDrop,
} from './dimension_panel';
import { IndexPatternDataPanel } from './datapanel';
@ -308,7 +308,7 @@ export function getIndexPatternDatasource({
domElement
);
},
getDropTypes,
getDropProps,
onDrop,
// Reset the temporary invalid state when closing the editor, but don't

View file

@ -189,9 +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;
getDropTypes: (
getDropProps: (
props: DatasourceDimensionDropProps<T> & { groupId: string }
) => DropType | undefined;
) => { dropType: DropType; nextLabel?: string } | undefined;
onDrop: (props: DatasourceDimensionDropHandlerProps<T>) => false | true | { deleted: string };
updateStateOnCloseDimension?: (props: {
layerId: string;