[Lens] (Accessibility) focus on adding/removing layers (#84900) (#86189)

This commit is contained in:
Marta Bondyra 2020-12-17 10:25:52 +01:00 committed by GitHub
parent ab1a480b4e
commit 9e1c76feb5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 653 additions and 423 deletions

View file

@ -0,0 +1,169 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { act } from 'react-dom/test-utils';
import {
createMockVisualization,
createMockFramePublicAPI,
createMockDatasource,
DatasourceMock,
} from '../../mocks';
import { Visualization } from '../../../types';
import { mountWithIntl } from '@kbn/test/jest';
import { LayerPanels } from './config_panel';
import { LayerPanel } from './layer_panel';
import { coreMock } from 'src/core/public/mocks';
import { generateId } from '../../../id_generator';
jest.mock('../../../id_generator');
describe('ConfigPanel', () => {
let mockVisualization: jest.Mocked<Visualization>;
let mockVisualization2: jest.Mocked<Visualization>;
let mockDatasource: DatasourceMock;
const frame = createMockFramePublicAPI();
function getDefaultProps() {
frame.datasourceLayers = {
first: mockDatasource.publicAPIMock,
};
return {
activeVisualizationId: 'vis1',
visualizationMap: {
vis1: mockVisualization,
vis2: mockVisualization2,
},
activeDatasourceId: 'ds1',
datasourceMap: {
ds1: mockDatasource,
},
activeVisualization: ({
...mockVisualization,
getLayerIds: () => Object.keys(frame.datasourceLayers),
appendLayer: true,
} as unknown) as Visualization,
datasourceStates: {
ds1: {
isLoading: false,
state: 'state',
},
},
visualizationState: 'state',
updateVisualization: jest.fn(),
updateDatasource: jest.fn(),
updateAll: jest.fn(),
framePublicAPI: frame,
dispatch: jest.fn(),
core: coreMock.createStart(),
};
}
beforeEach(() => {
mockVisualization = {
...createMockVisualization(),
id: 'testVis',
visualizationTypes: [
{
icon: 'empty',
id: 'testVis',
label: 'TEST1',
},
],
};
mockVisualization2 = {
...createMockVisualization(),
id: 'testVis2',
visualizationTypes: [
{
icon: 'empty',
id: 'testVis2',
label: 'TEST2',
},
],
};
mockVisualization.getLayerIds.mockReturnValue(Object.keys(frame.datasourceLayers));
mockDatasource = createMockDatasource('ds1');
});
describe('focus behavior when adding or removing layers', () => {
it('should focus the only layer when resetting the layer', () => {
const component = mountWithIntl(<LayerPanels {...getDefaultProps()} />);
const firstLayerFocusable = component
.find(LayerPanel)
.first()
.find('section')
.first()
.instance();
act(() => {
component.find('[data-test-subj="lnsLayerRemove"]').first().simulate('click');
});
const focusedEl = document.activeElement;
expect(focusedEl).toEqual(firstLayerFocusable);
});
it('should focus the second layer when removing the first layer', () => {
const defaultProps = getDefaultProps();
// overwriting datasourceLayers to test two layers
frame.datasourceLayers = {
first: mockDatasource.publicAPIMock,
second: mockDatasource.publicAPIMock,
};
const component = mountWithIntl(<LayerPanels {...defaultProps} />);
const secondLayerFocusable = component
.find(LayerPanel)
.at(1)
.find('section')
.first()
.instance();
act(() => {
component.find('[data-test-subj="lnsLayerRemove"]').at(0).simulate('click');
});
const focusedEl = document.activeElement;
expect(focusedEl).toEqual(secondLayerFocusable);
});
it('should focus the first layer when removing the second layer', () => {
const defaultProps = getDefaultProps();
// overwriting datasourceLayers to test two layers
frame.datasourceLayers = {
first: mockDatasource.publicAPIMock,
second: mockDatasource.publicAPIMock,
};
const component = mountWithIntl(<LayerPanels {...defaultProps} />);
const firstLayerFocusable = component
.find(LayerPanel)
.first()
.find('section')
.first()
.instance();
act(() => {
component.find('[data-test-subj="lnsLayerRemove"]').at(2).simulate('click');
});
const focusedEl = document.activeElement;
expect(focusedEl).toEqual(firstLayerFocusable);
});
it('should focus the added layer', () => {
(generateId as jest.Mock).mockReturnValue(`second`);
const dispatch = jest.fn((x) => {
if (x.subType === 'ADD_LAYER') {
frame.datasourceLayers.second = mockDatasource.publicAPIMock;
}
});
const component = mountWithIntl(<LayerPanels {...getDefaultProps()} dispatch={dispatch} />);
act(() => {
component.find('[data-test-subj="lnsLayerAddButton"]').first().simulate('click');
});
const focusedEl = document.activeElement;
expect(focusedEl?.children[0].getAttribute('data-test-subj')).toEqual('lns-layerPanel-1');
});
});
});

View file

@ -5,7 +5,7 @@
*/
import './config_panel.scss';
import React, { useMemo, memo } from 'react';
import React, { useMemo, memo, useEffect, useState, useCallback } from 'react';
import { EuiFlexItem, EuiToolTip, EuiButton, EuiForm } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Visualization } from '../../../types';
@ -24,7 +24,51 @@ export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: Config
) : null;
});
function LayerPanels(
function useFocusUpdate(layerIds: string[]) {
const [nextFocusedLayerId, setNextFocusedLayerId] = useState<string | null>(null);
const [layerRefs, setLayersRefs] = useState<Record<string, HTMLElement | null>>({});
useEffect(() => {
const focusable = nextFocusedLayerId && layerRefs[nextFocusedLayerId];
if (focusable) {
focusable.focus();
setNextFocusedLayerId(null);
}
}, [layerIds, layerRefs, nextFocusedLayerId]);
const setLayerRef = useCallback((layerId, el) => {
if (el) {
setLayersRefs((refs) => ({
...refs,
[layerId]: el,
}));
}
}, []);
const removeLayerRef = useCallback(
(layerId) => {
if (layerIds.length <= 1) {
return setNextFocusedLayerId(layerId);
}
const removedLayerIndex = layerIds.findIndex((l) => l === layerId);
const nextFocusedLayerIdId =
removedLayerIndex === 0 ? layerIds[1] : layerIds[removedLayerIndex - 1];
setLayersRefs((refs) => {
const newLayerRefs = { ...refs };
delete newLayerRefs[layerId];
return newLayerRefs;
});
return setNextFocusedLayerId(nextFocusedLayerIdId);
},
[layerIds]
);
return { setNextFocusedLayerId, removeLayerRef, setLayerRef };
}
export function LayerPanels(
props: ConfigPanelWrapperProps & {
activeDatasourceId: string;
activeVisualization: Visualization;
@ -37,6 +81,10 @@ function LayerPanels(
activeDatasourceId,
datasourceMap,
} = props;
const layerIds = activeVisualization.getLayerIds(visualizationState);
const { setNextFocusedLayerId, removeLayerRef, setLayerRef } = useFocusUpdate(layerIds);
const setVisualizationState = useMemo(
() => (newState: unknown) => {
dispatch({
@ -85,13 +133,13 @@ function LayerPanels(
},
[dispatch]
);
const layerIds = activeVisualization.getLayerIds(visualizationState);
return (
<EuiForm className="lnsConfigPanel">
{layerIds.map((layerId, index) => (
<LayerPanel
{...props}
setLayerRef={setLayerRef}
key={layerId}
layerId={layerId}
index={index}
@ -113,6 +161,7 @@ function LayerPanels(
state,
}),
});
removeLayerRef(layerId);
}}
/>
))}
@ -138,18 +187,20 @@ function LayerPanels(
defaultMessage: 'Add layer',
})}
onClick={() => {
const id = generateId();
dispatch({
type: 'UPDATE_STATE',
subType: 'ADD_LAYER',
updater: (state) =>
appendLayer({
activeVisualization,
generateId,
generateId: () => id,
trackUiEvent,
activeDatasource: datasourceMap[activeDatasourceId],
state,
}),
});
setNextFocusedLayerId(id);
}}
iconType="plusInCircleFilled"
/>

View file

@ -1,5 +1,10 @@
.lnsLayerPanel {
margin-bottom: $euiSizeS;
// disable focus ring for mouse clicks, leave it for keyboard users
&:focus:not(:focus-visible) {
animation: none !important; // sass-lint:disable-line no-important
}
}
.lnsLayerPanel__sourceFlexItem {

View file

@ -59,6 +59,7 @@ describe('LayerPanel', () => {
dispatch: jest.fn(),
core: coreMock.createStart(),
index: 0,
setLayerRef: jest.fn(),
};
}

View file

@ -74,6 +74,7 @@ export function LayerPanel(
newVisualizationState: unknown
) => void;
onRemoveLayer: () => void;
setLayerRef: (layerId: string, instance: HTMLDivElement | null) => void;
}
) {
const dragDropContext = useContext(DragContext);
@ -81,13 +82,18 @@ export function LayerPanel(
initialActiveDimensionState
);
const { framePublicAPI, layerId, isOnlyLayer, onRemoveLayer, index } = props;
const { framePublicAPI, layerId, isOnlyLayer, onRemoveLayer, setLayerRef, index } = props;
const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId];
useEffect(() => {
setActiveDimension(initialActiveDimensionState);
}, [props.activeVisualizationId]);
const setLayerRefMemoized = React.useCallback((el) => setLayerRef(layerId, el), [
layerId,
setLayerRef,
]);
if (
!datasourcePublicAPI ||
!props.activeVisualizationId ||
@ -131,467 +137,465 @@ export function LayerPanel(
const columnLabelMap = layerDatasource.uniqueLabels(layerDatasourceConfigProps.state);
return (
<ChildDragDropProvider {...dragDropContext}>
<EuiPanel
data-test-subj={`lns-layerPanel-${index}`}
className="lnsLayerPanel"
paddingSize="s"
>
<EuiFlexGroup gutterSize="s" alignItems="flexStart" responsive={false}>
<EuiFlexItem grow={false} className="lnsLayerPanel__settingsFlexItem">
<LayerSettings
layerId={layerId}
layerConfigProps={{
...layerVisualizationConfigProps,
setState: props.updateVisualization,
}}
activeVisualization={activeVisualization}
/>
</EuiFlexItem>
{layerDatasource && (
<EuiFlexItem className="lnsLayerPanel__sourceFlexItem">
<NativeRenderer
render={layerDatasource.renderLayerPanel}
nativeProps={{
layerId,
state: layerDatasourceState,
activeData: props.framePublicAPI.activeData,
setState: (updater: unknown) => {
const newState =
typeof updater === 'function' ? updater(layerDatasourceState) : updater;
// Look for removed columns
const nextPublicAPI = layerDatasource.getPublicAPI({
state: newState,
layerId,
});
const nextTable = new Set(
nextPublicAPI.getTableSpec().map(({ columnId }) => columnId)
);
const removed = datasourcePublicAPI
.getTableSpec()
.map(({ columnId }) => columnId)
.filter((columnId) => !nextTable.has(columnId));
let nextVisState = props.visualizationState;
removed.forEach((columnId) => {
nextVisState = activeVisualization.removeDimension({
layerId,
columnId,
prevState: nextVisState,
});
});
props.updateAll(datasourceId, newState, nextVisState);
},
<section tabIndex={-1} ref={setLayerRefMemoized} className="lnsLayerPanel">
<EuiPanel data-test-subj={`lns-layerPanel-${index}`} paddingSize="s">
<EuiFlexGroup gutterSize="s" alignItems="flexStart" responsive={false}>
<EuiFlexItem grow={false} className="lnsLayerPanel__settingsFlexItem">
<LayerSettings
layerId={layerId}
layerConfigProps={{
...layerVisualizationConfigProps,
setState: props.updateVisualization,
}}
activeVisualization={activeVisualization}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiSpacer size="m" />
{groups.map((group, groupIndex) => {
const newId = generateId();
const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0;
return (
<EuiFormRow
className={
group.supportsMoreColumns
? 'lnsLayerPanel__row'
: 'lnsLayerPanel__row lnsLayerPanel__row--notSupportsMoreColumns'
}
fullWidth
label={<div className="lnsLayerPanel__groupLabel">{group.groupLabel}</div>}
labelType="legend"
key={groupIndex}
isInvalid={isMissing}
error={
isMissing ? (
<div className="lnsLayerPanel__error">
{i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', {
defaultMessage: 'Required dimension',
})}
</div>
) : (
[]
)
}
>
<>
<ReorderProvider id={group.groupId} className={'lnsLayerPanel__group'}>
{group.accessors.map((accessorConfig) => {
const accessor = accessorConfig.columnId;
const { dragging } = dragDropContext;
const dragType =
isDraggedOperation(dragging) && accessor === dragging.columnId
? 'move'
: isDraggedOperation(dragging) && group.groupId === dragging.groupId
? 'reorder'
: 'copy';
const dropType = isDraggedOperation(dragging)
? group.groupId !== dragging.groupId
? 'replace'
: 'reorder'
: 'add';
const isFromCompatibleGroup =
dragging?.groupId !== group.groupId &&
layerDatasource.canHandleDrop({
...layerDatasourceDropProps,
columnId: accessor,
filterOperations: group.filterOperations,
{layerDatasource && (
<EuiFlexItem className="lnsLayerPanel__sourceFlexItem">
<NativeRenderer
render={layerDatasource.renderLayerPanel}
nativeProps={{
layerId,
state: layerDatasourceState,
activeData: props.framePublicAPI.activeData,
setState: (updater: unknown) => {
const newState =
typeof updater === 'function' ? updater(layerDatasourceState) : updater;
// Look for removed columns
const nextPublicAPI = layerDatasource.getPublicAPI({
state: newState,
layerId,
});
const nextTable = new Set(
nextPublicAPI.getTableSpec().map(({ columnId }) => columnId)
);
const removed = datasourcePublicAPI
.getTableSpec()
.map(({ columnId }) => columnId)
.filter((columnId) => !nextTable.has(columnId));
let nextVisState = props.visualizationState;
removed.forEach((columnId) => {
nextVisState = activeVisualization.removeDimension({
layerId,
columnId,
prevState: nextVisState,
});
});
const isFromTheSameGroup =
isDraggedOperation(dragging) &&
dragging.groupId === group.groupId &&
dragging.columnId !== accessor;
props.updateAll(datasourceId, newState, nextVisState);
},
}}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
const isDroppable = isDraggedOperation(dragging)
? dragType === 'reorder'
? isFromTheSameGroup
: isFromCompatibleGroup
: layerDatasource.canHandleDrop({
<EuiSpacer size="m" />
{groups.map((group, groupIndex) => {
const newId = generateId();
const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0;
return (
<EuiFormRow
className={
group.supportsMoreColumns
? 'lnsLayerPanel__row'
: 'lnsLayerPanel__row lnsLayerPanel__row--notSupportsMoreColumns'
}
fullWidth
label={<div className="lnsLayerPanel__groupLabel">{group.groupLabel}</div>}
labelType="legend"
key={groupIndex}
isInvalid={isMissing}
error={
isMissing ? (
<div className="lnsLayerPanel__error">
{i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', {
defaultMessage: 'Required dimension',
})}
</div>
) : (
[]
)
}
>
<>
<ReorderProvider id={group.groupId} className={'lnsLayerPanel__group'}>
{group.accessors.map((accessorConfig) => {
const accessor = accessorConfig.columnId;
const { dragging } = dragDropContext;
const dragType =
isDraggedOperation(dragging) && accessor === dragging.columnId
? 'move'
: isDraggedOperation(dragging) && group.groupId === dragging.groupId
? 'reorder'
: 'copy';
const dropType = isDraggedOperation(dragging)
? group.groupId !== dragging.groupId
? 'replace'
: 'reorder'
: 'add';
const isFromCompatibleGroup =
dragging?.groupId !== group.groupId &&
layerDatasource.canHandleDrop({
...layerDatasourceDropProps,
columnId: accessor,
filterOperations: group.filterOperations,
});
return (
<DragDrop
key={accessor}
draggable={!activeId}
dragType={dragType}
dropType={dropType}
data-test-subj={group.dataTestSubj}
itemsInGroup={group.accessors.map((a) =>
typeof a === 'string' ? a : a.columnId
)}
className={'lnsLayerPanel__dimensionContainer'}
value={{
columnId: accessor,
groupId: group.groupId,
layerId,
id: accessor,
}}
isValueEqual={isSameConfiguration}
label={columnLabelMap[accessor]}
droppable={dragging && isDroppable}
dropTo={(dropTargetId: string) => {
layerDatasource.onDrop({
isReorder: true,
...layerDatasourceDropProps,
droppedItem: {
columnId: accessor,
groupId: group.groupId,
layerId,
id: accessor,
},
columnId: dropTargetId,
filterOperations: group.filterOperations,
});
}}
onDrop={(droppedItem) => {
const isReorder =
isDraggedOperation(droppedItem) &&
droppedItem.groupId === group.groupId &&
droppedItem.columnId !== accessor;
const isFromTheSameGroup =
isDraggedOperation(dragging) &&
dragging.groupId === group.groupId &&
dragging.columnId !== accessor;
const dropResult = layerDatasource.onDrop({
isReorder,
const isDroppable = isDraggedOperation(dragging)
? dragType === 'reorder'
? isFromTheSameGroup
: isFromCompatibleGroup
: layerDatasource.canHandleDrop({
...layerDatasourceDropProps,
droppedItem,
columnId: accessor,
filterOperations: group.filterOperations,
});
if (typeof dropResult === 'object') {
// When a column is moved, we delete the reference to the old
props.updateVisualization(
activeVisualization.removeDimension({
return (
<DragDrop
key={accessor}
draggable={!activeId}
dragType={dragType}
dropType={dropType}
data-test-subj={group.dataTestSubj}
itemsInGroup={group.accessors.map((a) =>
typeof a === 'string' ? a : a.columnId
)}
className={'lnsLayerPanel__dimensionContainer'}
value={{
columnId: accessor,
groupId: group.groupId,
layerId,
id: accessor,
}}
isValueEqual={isSameConfiguration}
label={columnLabelMap[accessor]}
droppable={dragging && isDroppable}
dropTo={(dropTargetId: string) => {
layerDatasource.onDrop({
isReorder: true,
...layerDatasourceDropProps,
droppedItem: {
columnId: accessor,
groupId: group.groupId,
layerId,
columnId: dropResult.deleted,
id: accessor,
},
columnId: dropTargetId,
filterOperations: group.filterOperations,
});
}}
onDrop={(droppedItem) => {
const isReorder =
isDraggedOperation(droppedItem) &&
droppedItem.groupId === group.groupId &&
droppedItem.columnId !== accessor;
const dropResult = layerDatasource.onDrop({
isReorder,
...layerDatasourceDropProps,
droppedItem,
columnId: accessor,
filterOperations: group.filterOperations,
});
if (typeof dropResult === 'object') {
// When a column is moved, we delete the reference to the old
props.updateVisualization(
activeVisualization.removeDimension({
layerId,
columnId: dropResult.deleted,
prevState: props.visualizationState,
})
);
}
}}
>
<div className="lnsLayerPanel__dimension">
<EuiLink
className="lnsLayerPanel__dimensionLink"
data-test-subj="lnsLayerPanel-dimensionLink"
onClick={() => {
if (activeId) {
setActiveDimension(initialActiveDimensionState);
} else {
setActiveDimension({
isNew: false,
activeGroup: group,
activeId: accessor,
});
}
}}
aria-label={triggerLinkA11yText(columnLabelMap[accessor])}
title={triggerLinkA11yText(columnLabelMap[accessor])}
>
<ColorIndicator accessorConfig={accessorConfig}>
<NativeRenderer
render={layerDatasource.renderDimensionTrigger}
nativeProps={{
...layerDatasourceConfigProps,
columnId: accessor,
filterOperations: group.filterOperations,
}}
/>
</ColorIndicator>
</EuiLink>
<EuiButtonIcon
className="lnsLayerPanel__dimensionRemove"
data-test-subj="indexPattern-dimension-remove"
iconType="cross"
iconSize="s"
size="s"
color="danger"
aria-label={i18n.translate(
'xpack.lens.indexPattern.removeColumnLabel',
{
defaultMessage: 'Remove configuration from "{groupLabel}"',
values: { groupLabel: group.groupLabel },
}
)}
title={i18n.translate('xpack.lens.indexPattern.removeColumnLabel', {
defaultMessage: 'Remove configuration from "{groupLabel}"',
values: { groupLabel: group.groupLabel },
})}
onClick={() => {
trackUiEvent('indexpattern_dimension_removed');
props.updateAll(
datasourceId,
layerDatasource.removeColumn({
layerId,
columnId: accessor,
prevState: layerDatasourceState,
}),
activeVisualization.removeDimension({
layerId,
columnId: accessor,
prevState: props.visualizationState,
})
);
}}
/>
<PaletteIndicator accessorConfig={accessorConfig} />
</div>
</DragDrop>
);
})}
</ReorderProvider>
{group.supportsMoreColumns ? (
<div className={'lnsLayerPanel__dimensionContainer'}>
<DragDrop
data-test-subj={group.dataTestSubj}
droppable={
Boolean(dragDropContext.dragging) &&
// Verify that the dragged item is not coming from the same group
// since this would be a reorder
(!isDraggedOperation(dragDropContext.dragging) ||
dragDropContext.dragging.groupId !== group.groupId) &&
layerDatasource.canHandleDrop({
...layerDatasourceDropProps,
columnId: newId,
filterOperations: group.filterOperations,
})
}
onDrop={(droppedItem) => {
const dropResult = layerDatasource.onDrop({
...layerDatasourceDropProps,
droppedItem,
columnId: newId,
filterOperations: group.filterOperations,
});
if (dropResult) {
props.updateVisualization(
activeVisualization.setDimension({
layerId,
groupId: group.groupId,
columnId: newId,
prevState: props.visualizationState,
})
);
if (typeof dropResult === 'object') {
// When a column is moved, we delete the reference to the old
props.updateVisualization(
activeVisualization.removeDimension({
layerId,
columnId: dropResult.deleted,
prevState: props.visualizationState,
})
);
}
}
}}
>
<div className="lnsLayerPanel__dimension">
<EuiLink
className="lnsLayerPanel__dimensionLink"
data-test-subj="lnsLayerPanel-dimensionLink"
<div className="lnsLayerPanel__dimension lnsLayerPanel__dimension--empty">
<EuiButtonEmpty
className="lnsLayerPanel__triggerText"
color="text"
size="xs"
iconType="plusInCircleFilled"
contentProps={{
className: 'lnsLayerPanel__triggerTextContent',
}}
aria-label={i18n.translate(
'xpack.lens.indexPattern.removeColumnAriaLabel',
{
defaultMessage: 'Drop a field or click to add to {groupLabel}',
values: { groupLabel: group.groupLabel },
}
)}
data-test-subj="lns-empty-dimension"
onClick={() => {
if (activeId) {
setActiveDimension(initialActiveDimensionState);
} else {
setActiveDimension({
isNew: false,
isNew: true,
activeGroup: group,
activeId: accessor,
activeId: newId,
});
}
}}
aria-label={triggerLinkA11yText(columnLabelMap[accessor])}
title={triggerLinkA11yText(columnLabelMap[accessor])}
>
<ColorIndicator accessorConfig={accessorConfig}>
<NativeRenderer
render={layerDatasource.renderDimensionTrigger}
nativeProps={{
...layerDatasourceConfigProps,
columnId: accessor,
filterOperations: group.filterOperations,
}}
/>
</ColorIndicator>
</EuiLink>
<EuiButtonIcon
className="lnsLayerPanel__dimensionRemove"
data-test-subj="indexPattern-dimension-remove"
iconType="cross"
iconSize="s"
size="s"
color="danger"
aria-label={i18n.translate(
'xpack.lens.indexPattern.removeColumnLabel',
{
defaultMessage: 'Remove configuration from "{groupLabel}"',
values: { groupLabel: group.groupLabel },
}
)}
title={i18n.translate('xpack.lens.indexPattern.removeColumnLabel', {
defaultMessage: 'Remove configuration from "{groupLabel}"',
values: { groupLabel: group.groupLabel },
})}
onClick={() => {
trackUiEvent('indexpattern_dimension_removed');
props.updateAll(
datasourceId,
layerDatasource.removeColumn({
layerId,
columnId: accessor,
prevState: layerDatasourceState,
}),
activeVisualization.removeDimension({
layerId,
columnId: accessor,
prevState: props.visualizationState,
})
);
}}
/>
<PaletteIndicator accessorConfig={accessorConfig} />
<FormattedMessage
id="xpack.lens.configure.emptyConfig"
defaultMessage="Drop a field or click to add"
/>
</EuiButtonEmpty>
</div>
</DragDrop>
);
})}
</ReorderProvider>
{group.supportsMoreColumns ? (
<div className={'lnsLayerPanel__dimensionContainer'}>
<DragDrop
data-test-subj={group.dataTestSubj}
droppable={
Boolean(dragDropContext.dragging) &&
// Verify that the dragged item is not coming from the same group
// since this would be a reorder
(!isDraggedOperation(dragDropContext.dragging) ||
dragDropContext.dragging.groupId !== group.groupId) &&
layerDatasource.canHandleDrop({
...layerDatasourceDropProps,
columnId: newId,
filterOperations: group.filterOperations,
})
}
onDrop={(droppedItem) => {
const dropResult = layerDatasource.onDrop({
...layerDatasourceDropProps,
droppedItem,
columnId: newId,
filterOperations: group.filterOperations,
});
if (dropResult) {
props.updateVisualization(
</div>
) : null}
</>
</EuiFormRow>
);
})}
<DimensionContainer
isOpen={!!activeId}
groupLabel={activeGroup?.groupLabel || ''}
handleClose={() => {
if (layerDatasource.updateStateOnCloseDimension) {
const newState = layerDatasource.updateStateOnCloseDimension({
state: layerDatasourceState,
layerId,
columnId: activeId!,
});
if (newState) {
props.updateDatasource(datasourceId, newState);
}
}
setActiveDimension(initialActiveDimensionState);
}}
panel={
<>
{activeGroup && activeId && (
<NativeRenderer
render={layerDatasource.renderDimensionEditor}
nativeProps={{
...layerDatasourceConfigProps,
core: props.core,
columnId: activeId,
filterOperations: activeGroup.filterOperations,
dimensionGroups: groups,
setState: (newState: unknown, shouldUpdateVisualization?: boolean) => {
if (shouldUpdateVisualization) {
props.updateAll(
datasourceId,
newState,
activeVisualization.setDimension({
layerId,
groupId: group.groupId,
columnId: newId,
groupId: activeGroup.groupId,
columnId: activeId,
prevState: props.visualizationState,
})
);
if (typeof dropResult === 'object') {
// When a column is moved, we delete the reference to the old
props.updateVisualization(
activeVisualization.removeDimension({
layerId,
columnId: dropResult.deleted,
prevState: props.visualizationState,
})
);
}
} else {
props.updateDatasource(datasourceId, newState);
}
}}
>
<div className="lnsLayerPanel__dimension lnsLayerPanel__dimension--empty">
<EuiButtonEmpty
className="lnsLayerPanel__triggerText"
color="text"
size="xs"
iconType="plusInCircleFilled"
contentProps={{
className: 'lnsLayerPanel__triggerTextContent',
}}
aria-label={i18n.translate(
'xpack.lens.indexPattern.removeColumnAriaLabel',
{
defaultMessage: 'Drop a field or click to add to {groupLabel}',
values: { groupLabel: group.groupLabel },
}
)}
data-test-subj="lns-empty-dimension"
onClick={() => {
if (activeId) {
setActiveDimension(initialActiveDimensionState);
} else {
setActiveDimension({
isNew: true,
activeGroup: group,
activeId: newId,
});
}
}}
>
<FormattedMessage
id="xpack.lens.configure.emptyConfig"
defaultMessage="Drop a field or click to add"
/>
</EuiButtonEmpty>
</div>
</DragDrop>
</div>
) : null}
</>
</EuiFormRow>
);
})}
<DimensionContainer
isOpen={!!activeId}
groupLabel={activeGroup?.groupLabel || ''}
handleClose={() => {
if (layerDatasource.updateStateOnCloseDimension) {
const newState = layerDatasource.updateStateOnCloseDimension({
state: layerDatasourceState,
layerId,
columnId: activeId!,
});
if (newState) {
props.updateDatasource(datasourceId, newState);
}
}
setActiveDimension(initialActiveDimensionState);
}}
panel={
<>
{activeGroup && activeId && (
<NativeRenderer
render={layerDatasource.renderDimensionEditor}
nativeProps={{
...layerDatasourceConfigProps,
core: props.core,
columnId: activeId,
filterOperations: activeGroup.filterOperations,
dimensionGroups: groups,
setState: (newState: unknown, shouldUpdateVisualization?: boolean) => {
if (shouldUpdateVisualization) {
props.updateAll(
datasourceId,
newState,
activeVisualization.setDimension({
layerId,
groupId: activeGroup.groupId,
columnId: activeId,
prevState: props.visualizationState,
})
);
} else {
props.updateDatasource(datasourceId, newState);
}
setActiveDimension({
...activeDimension,
isNew: false,
});
},
}}
/>
)}
{activeGroup &&
activeId &&
!activeDimension.isNew &&
activeVisualization.renderDimensionEditor &&
activeGroup?.enableDimensionEditor && (
<div className="lnsLayerPanel__styleEditor">
<NativeRenderer
render={activeVisualization.renderDimensionEditor}
nativeProps={{
...layerVisualizationConfigProps,
groupId: activeGroup.groupId,
accessor: activeId,
setState: props.updateVisualization,
}}
/>
</div>
setActiveDimension({
...activeDimension,
isNew: false,
});
},
}}
/>
)}
</>
}
/>
{activeGroup &&
activeId &&
!activeDimension.isNew &&
activeVisualization.renderDimensionEditor &&
activeGroup?.enableDimensionEditor && (
<div className="lnsLayerPanel__styleEditor">
<NativeRenderer
render={activeVisualization.renderDimensionEditor}
nativeProps={{
...layerVisualizationConfigProps,
groupId: activeGroup.groupId,
accessor: activeId,
setState: props.updateVisualization,
}}
/>
</div>
)}
</>
}
/>
<EuiSpacer size="m" />
<EuiSpacer size="m" />
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="xs"
iconType="trash"
color="danger"
data-test-subj="lnsLayerRemove"
aria-label={
isOnlyLayer
? i18n.translate('xpack.lens.resetLayerAriaLabel', {
defaultMessage: 'Reset layer {index}',
values: { index: index + 1 },
})
: i18n.translate('xpack.lens.deleteLayerAriaLabel', {
defaultMessage: `Delete layer {index}`,
values: { index: index + 1 },
})
}
onClick={() => {
// If we don't blur the remove / clear button, it remains focused
// which is a strange UX in this case. e.target.blur doesn't work
// due to who knows what, but probably event re-writing. Additionally,
// activeElement does not have blur so, we need to do some casting + safeguards.
const el = (document.activeElement as unknown) as { blur: () => void };
if (el?.blur) {
el.blur();
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="xs"
iconType="trash"
color="danger"
data-test-subj="lnsLayerRemove"
aria-label={
isOnlyLayer
? i18n.translate('xpack.lens.resetLayerAriaLabel', {
defaultMessage: 'Reset layer {index}',
values: { index: index + 1 },
})
: i18n.translate('xpack.lens.deleteLayerAriaLabel', {
defaultMessage: `Delete layer {index}`,
values: { index: index + 1 },
})
}
onClick={() => {
// If we don't blur the remove / clear button, it remains focused
// which is a strange UX in this case. e.target.blur doesn't work
// due to who knows what, but probably event re-writing. Additionally,
// activeElement does not have blur so, we need to do some casting + safeguards.
const el = (document.activeElement as unknown) as { blur: () => void };
onRemoveLayer();
}}
>
{isOnlyLayer
? i18n.translate('xpack.lens.resetLayer', {
defaultMessage: 'Reset layer',
})
: i18n.translate('xpack.lens.deleteLayer', {
defaultMessage: `Delete layer`,
})}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
if (el?.blur) {
el.blur();
}
onRemoveLayer();
}}
>
{isOnlyLayer
? i18n.translate('xpack.lens.resetLayer', {
defaultMessage: 'Reset layer',
})
: i18n.translate('xpack.lens.deleteLayer', {
defaultMessage: `Delete layer`,
})}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</section>
</ChildDragDropProvider>
);
}

View file

@ -602,7 +602,7 @@ describe('editor_frame', () => {
});
// validation requires to calls this getConfiguration API
expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(6);
expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(7);
expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith(
expect.objectContaining({
state: updatedState,
@ -682,7 +682,7 @@ describe('editor_frame', () => {
});
// validation requires to calls this getConfiguration API
expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(6);
expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(7);
expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith(
expect.objectContaining({
frame: expect.objectContaining({
@ -1196,7 +1196,7 @@ describe('editor_frame', () => {
});
// validation requires to calls this getConfiguration API
expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(4);
expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(5);
expect(mockVisualization.getConfiguration).toHaveBeenCalledWith(
expect.objectContaining({
state: suggestionVisState,

View file

@ -52,7 +52,7 @@
.lnsConfigPanel {
@include euiScrollBar;
padding: $euiSize 0 $euiSize $euiSize;
padding: $euiSize $euiSizeXS $euiSize $euiSize;
overflow-x: hidden;
overflow-y: scroll;
}