Skip to content

Commit

Permalink
[Lens] (Accessibility) focus on adding/removing layers (#84900) (#86189)
Browse files Browse the repository at this point in the history
  • Loading branch information
mbondyra committed Dec 17, 2020
1 parent ab1a480 commit 9e1c76f
Show file tree
Hide file tree
Showing 7 changed files with 643 additions and 413 deletions.
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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({
Expand Down Expand Up @@ -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}
Expand All @@ -113,6 +161,7 @@ function LayerPanels(
state,
}),
});
removeLayerRef(layerId);
}}
/>
))}
Expand All @@ -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"
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ describe('LayerPanel', () => {
dispatch: jest.fn(),
core: coreMock.createStart(),
index: 0,
setLayerRef: jest.fn(),
};
}

Expand Down
Loading

0 comments on commit 9e1c76f

Please sign in to comment.