Skip to content

Commit

Permalink
focus management
Browse files Browse the repository at this point in the history
  • Loading branch information
mbondyra committed Feb 7, 2021
1 parent 228c30e commit 763dc90
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import './config_panel.scss';

import React, { useMemo, memo, useEffect, useState, useCallback } from 'react';
import React, { useMemo, memo } from 'react';
import { EuiFlexItem, EuiToolTip, EuiButton, EuiForm } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Visualization } from '../../../types';
Expand All @@ -16,6 +16,7 @@ import { trackUiEvent } from '../../../lens_ui_telemetry';
import { generateId } from '../../../id_generator';
import { removeLayer, appendLayer } from './layer_actions';
import { ConfigPanelWrapperProps } from './types';
import { useFocusUpdate } from './use_focus_update';

export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) {
const activeVisualization = props.visualizationMap[props.activeVisualizationId || ''];
Expand All @@ -26,50 +27,6 @@ export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: Config
) : null;
});

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;
Expand All @@ -85,7 +42,11 @@ export function LayerPanels(
} = props;

const layerIds = activeVisualization.getLayerIds(visualizationState);
const { setNextFocusedLayerId, removeLayerRef, setLayerRef } = useFocusUpdate(layerIds);
const {
setNextFocusedId: setNextFocusedLayerId,
removeRef: removeLayerRef,
registerNewRef: registerNewLayerRef,
} = useFocusUpdate(layerIds);

const setVisualizationState = useMemo(
() => (newState: unknown) => {
Expand Down Expand Up @@ -145,7 +106,7 @@ export function LayerPanels(
<LayerPanel
{...props}
activeVisualization={activeVisualization}
setLayerRef={setLayerRef}
registerNewLayerRef={registerNewLayerRef}
key={layerId}
layerId={layerId}
layerIndex={layerIndex}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React, { useMemo } from 'react';
import React, { useMemo, useCallback } from 'react';
import { DragDrop, DragDropIdentifier, DragContextState } from '../../../drag_drop';
import {
Datasource,
Expand Down Expand Up @@ -44,6 +44,7 @@ export function DraggableDimensionButton({
dragDropContext,
layerDatasourceDropProps,
layerDatasource,
registerNewButtonRef,
}: {
dragDropContext: DragContextState;
layerId: string;
Expand All @@ -61,6 +62,7 @@ export function DraggableDimensionButton({
layerDatasourceDropProps: LayerDatasourceDropProps;
accessorIndex: number;
columnId: string;
registerNewButtonRef: (id: string, instance: HTMLDivElement | null) => void;
}) {
const dropType = layerDatasource.getDropTypes({
...layerDatasourceDropProps,
Expand Down Expand Up @@ -94,8 +96,17 @@ export function DraggableDimensionButton({
[group.accessors]
);

const registerNewButtonRefMemoized = useCallback((el) => registerNewButtonRef(columnId, el), [
registerNewButtonRef,
columnId,
]);

return (
<div className="lnsLayerPanel__dimensionContainer" data-test-subj={group.dataTestSubj}>
<div
ref={registerNewButtonRefMemoized}
className="lnsLayerPanel__dimensionContainer"
data-test-subj={group.dataTestSubj}
>
<DragDrop
getAdditionalClassesOnEnter={getAdditionalClassesOnEnter}
getAdditionalClassesOnDroppable={getAdditionalClassesOnDroppable}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ describe('LayerPanel', () => {
dispatch: jest.fn(),
core: coreMock.createStart(),
layerIndex: 0,
setLayerRef: jest.fn(),
registerNewLayerRef: jest.fn(),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { RemoveLayerButton } from './remove_layer_button';
import { EmptyDimensionButton } from './empty_dimension_button';
import { DimensionButton } from './dimension_button';
import { DraggableDimensionButton } from './draggable_dimension_button';
import { useFocusUpdate } from './use_focus_update';

const initialActiveDimensionState = {
isNew: false,
Expand All @@ -45,7 +46,7 @@ export function LayerPanel(
newVisualizationState: unknown
) => void;
onRemoveLayer: () => void;
setLayerRef: (layerId: string, instance: HTMLDivElement | null) => void;
registerNewLayerRef: (layerId: string, instance: HTMLDivElement | null) => void;
}
) {
const dragDropContext = useContext(DragContext);
Expand All @@ -58,7 +59,7 @@ export function LayerPanel(
layerId,
isOnlyLayer,
onRemoveLayer,
setLayerRef,
registerNewLayerRef,
layerIndex,
activeVisualization,
updateVisualization,
Expand All @@ -70,7 +71,10 @@ export function LayerPanel(
setActiveDimension(initialActiveDimensionState);
}, [activeVisualization.id]);

const setLayerRefMemoized = useCallback((el) => setLayerRef(layerId, el), [layerId, setLayerRef]);
const registerLayerRef = useCallback((el) => registerNewLayerRef(layerId, el), [
layerId,
registerNewLayerRef,
]);

const layerVisualizationConfigProps = {
layerId,
Expand Down Expand Up @@ -114,6 +118,16 @@ export function LayerPanel(
const { setDimension, removeDimension } = activeVisualization;
const layerDatasourceOnDrop = layerDatasource.onDrop;

const allAccessors = groups.flatMap((group) =>
group.accessors.map((accessor) => accessor.columnId)
);

const {
setNextFocusedId: setNextButtonId,
removeRef: removeButtonRef,
registerNewRef: registerNewButtonRef,
} = useFocusUpdate(allAccessors);

const onDrop = useMemo(() => {
return (
droppedItem: DragDropIdentifier,
Expand All @@ -127,7 +141,13 @@ export function LayerPanel(
columnId,
groupId,
layerId: targetLayerId,
} = (targetItem as unknown) as DraggedOperation; // TODO: correct misleading name
} = (targetItem as unknown) as DraggedOperation;
// for what operations we want to leave focus on drag and for what move it to drop?
if (dropType === 'reorder' || dropType === 'field_replace' || dropType === 'field_add') {
setNextButtonId(droppedItem.id);
} else {
setNextButtonId(columnId);
}

const filterOperations =
groups.find(({ groupId: gId }) => gId === targetItem.groupId)?.filterOperations ||
Expand Down Expand Up @@ -171,11 +191,12 @@ export function LayerPanel(
setDimension,
removeDimension,
layerDatasourceDropProps,
setNextButtonId,
]);

return (
<ChildDragDropProvider {...dragDropContext}>
<section tabIndex={-1} ref={setLayerRefMemoized} className="lnsLayerPanel">
<section tabIndex={-1} ref={registerLayerRef} className="lnsLayerPanel">
<EuiPanel data-test-subj={`lns-layerPanel-${layerIndex}`} paddingSize="s">
<EuiFlexGroup gutterSize="s" alignItems="flexStart" responsive={false}>
<EuiFlexItem grow={false} className="lnsLayerPanel__settingsFlexItem">
Expand Down Expand Up @@ -264,6 +285,7 @@ export function LayerPanel(

return (
<DraggableDimensionButton
registerNewButtonRef={registerNewButtonRef}
accessorIndex={accessorIndex}
columnId={columnId}
dragDropContext={dragDropContext}
Expand Down Expand Up @@ -304,6 +326,7 @@ export function LayerPanel(
prevState: props.visualizationState,
})
);
removeButtonRef(id);
}}
>
<NativeRenderer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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 { useEffect, useState, useCallback } from 'react';

const focusableSelector = 'button, [href], input, select, textarea, [tabindex]';

const getFirstFocusable = (el: HTMLElement | null) => {
if (!el) {
return null;
}
const firstFocusable = el.querySelector(focusableSelector);
if (!firstFocusable) {
return null;
}
return (firstFocusable as unknown) as { focus: () => void };
};

export function useFocusUpdate(ids: string[]) {
const [nextFocusedId, setNextFocusedId] = useState<string | null>(null);
const [refsById, setRefsById] = useState<Record<string, HTMLElement | null>>({});

useEffect(() => {
const element = nextFocusedId && refsById[nextFocusedId];
if (element) {
const focusable = element.matches(focusableSelector) ? element : getFirstFocusable(element);
focusable?.focus();
setNextFocusedId(null);
}
}, [ids, refsById, nextFocusedId]);

const registerNewRef = useCallback((id, el) => {
if (el) {
setRefsById((r) => ({
...r,
[id]: el,
}));
}
}, []);

const removeRef = useCallback(
(id) => {
if (ids.length <= 1) {
return setNextFocusedId(id);
}

const removedIndex = ids.findIndex((l) => l === id);
const next = removedIndex === 0 ? ids[1] : ids[removedIndex - 1];

setRefsById((refs) => {
const newRefsById = { ...refs };
delete newRefsById[id];
return newRefsById;
});
return setNextFocusedId(next);
},
[ids]
);

return { setNextFocusedId, removeRef, registerNewRef };
}

0 comments on commit 763dc90

Please sign in to comment.