[Index management] Fix a11y focus order in index mappings page (#203361)

## Summary

Fix a11y focus order in index mappings page. When new field is in
pending state and after closing edit pending field Flyout.


https://github.com/user-attachments/assets/dbdf59e5-0ebd-47e0-9b5e-19ab1556e771

### Test instructions 
#### Adding a field
1. Add new field in index mappings page by navigating via tab 
2. Notice that type fields combo box is focused
3. Add field and click to Add field button again with in pending fields
form
4. Notice focus is on new create field form

#### Edit field in pending state
1. Add new fields via tab key
2. click on edit field 
3. Try closing, updating and cancelling in the edit field flyout form
4. Notice after edit field flyout is closed, focus is on the pending
fields form
This commit is contained in:
Saarika Bhasi 2024-12-10 10:46:20 -05:00 committed by GitHub
parent 6e57a23d18
commit 4b0c0e9269
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 51 additions and 8 deletions

View file

@ -27,6 +27,7 @@ interface Props {
onCancelAddingNewFields?: () => void;
isAddingFields?: boolean;
semanticTextInfo?: SemanticTextInfo;
pendingFieldsRef?: React.RefObject<HTMLDivElement>;
}
export const DocumentFields = React.memo(
({
@ -35,6 +36,7 @@ export const DocumentFields = React.memo(
onCancelAddingNewFields,
isAddingFields,
semanticTextInfo,
pendingFieldsRef,
}: Props) => {
const { fields, documentFields } = useMappingsState();
const dispatch = useDispatch();
@ -58,6 +60,7 @@ export const DocumentFields = React.memo(
onCancelAddingNewFields={onCancelAddingNewFields}
isAddingFields={isAddingFields}
semanticTextInfo={semanticTextInfo}
pendingFieldsRef={pendingFieldsRef}
/>
);
@ -81,8 +84,9 @@ export const DocumentFields = React.memo(
useEffect(() => {
if (!isEditing) {
removeContentFromGlobalFlyout('mappingsEditField');
if (pendingFieldsRef?.current) pendingFieldsRef.current.focus();
}
}, [isEditing, removeContentFromGlobalFlyout]);
}, [isEditing, removeContentFromGlobalFlyout, pendingFieldsRef]);
useEffect(() => {
return () => {

View file

@ -24,6 +24,7 @@ interface Props {
isMultiField?: boolean | null;
showDocLink?: boolean;
isSemanticTextEnabled?: boolean;
fieldTypeInputRef?: React.MutableRefObject<HTMLInputElement | null>;
}
export const TypeParameter = ({
@ -31,6 +32,7 @@ export const TypeParameter = ({
isRootLevelField,
showDocLink = false,
isSemanticTextEnabled = true,
fieldTypeInputRef,
}: Props) => {
const fieldTypeOptions = useMemo(() => {
let options = isMultiField
@ -97,6 +99,9 @@ export const TypeParameter = ({
onChange={typeField.setValue}
isClearable={false}
data-test-subj="fieldType"
inputRef={(input) => {
if (fieldTypeInputRef) fieldTypeInputRef.current = input;
}}
/>
</EuiFormRow>
);

View file

@ -17,7 +17,7 @@ import { i18n } from '@kbn/i18n';
import { TrainedModelStat } from '@kbn/ml-plugin/common/types/trained_models';
import { MlPluginStart } from '@kbn/ml-plugin/public';
import classNames from 'classnames';
import React, { useEffect } from 'react';
import React, { useEffect, useRef } from 'react';
import { EUI_SIZE, TYPE_DEFINITION } from '../../../../constants';
import { fieldSerializer } from '../../../../lib';
import { isSemanticTextField } from '../../../../lib/utils';
@ -62,6 +62,7 @@ interface Props {
onCancelAddingNewFields?: () => void;
isAddingFields?: boolean;
semanticTextInfo?: SemanticTextInfo;
createFieldFormRef?: React.RefObject<HTMLDivElement>;
}
export const CreateField = React.memo(function CreateFieldComponent({
@ -74,9 +75,11 @@ export const CreateField = React.memo(function CreateFieldComponent({
onCancelAddingNewFields,
isAddingFields,
semanticTextInfo,
createFieldFormRef,
}: Props) {
const { isSemanticTextEnabled, ml, setErrorsInTrainedModelDeployment } = semanticTextInfo ?? {};
const dispatch = useDispatch();
const fieldTypeInputRef = useRef<HTMLInputElement>(null);
const { form } = useForm<Field>({
serializer: fieldSerializer,
@ -111,6 +114,10 @@ export const CreateField = React.memo(function CreateFieldComponent({
const isSemanticText = form.getFormData().type === 'semantic_text';
useEffect(() => {
if (createFieldFormRef?.current) createFieldFormRef?.current.focus();
}, [createFieldFormRef]);
const submitForm = async (
e?: React.FormEvent,
exitAfter: boolean = false,
@ -134,6 +141,10 @@ export const CreateField = React.memo(function CreateFieldComponent({
}
form.reset();
}
if (fieldTypeInputRef.current) {
fieldTypeInputRef.current.focus();
}
};
const onClickOutside = () => {
@ -157,6 +168,7 @@ export const CreateField = React.memo(function CreateFieldComponent({
isMultiField={isMultiField}
showDocLink
isSemanticTextEnabled={isSemanticTextEnabled}
fieldTypeInputRef={fieldTypeInputRef}
/>
</EuiFlexItem>
@ -266,6 +278,8 @@ export const CreateField = React.memo(function CreateFieldComponent({
: paddingLeft
}px`,
}}
ref={createFieldFormRef}
tabIndex={0}
>
<div className="mappingsEditor__createFieldContent">
{renderFormFields()}

View file

@ -16,6 +16,7 @@ interface Props {
state: State;
setPreviousState?: (state: State) => void;
isAddingFields?: boolean;
pendingFieldsRef?: React.RefObject<HTMLDivElement>;
}
export const FieldsList = React.memo(function FieldsListComponent({
@ -24,6 +25,7 @@ export const FieldsList = React.memo(function FieldsListComponent({
state,
setPreviousState,
isAddingFields,
pendingFieldsRef,
}: Props) {
if (fields === undefined) {
return null;
@ -39,6 +41,7 @@ export const FieldsList = React.memo(function FieldsListComponent({
state={state}
setPreviousState={setPreviousState}
isAddingFields={isAddingFields}
pendingFieldsRef={pendingFieldsRef}
/>
))}
</ul>

View file

@ -64,6 +64,8 @@ interface Props {
treeDepth: number;
state: State;
isAddingFields?: boolean;
createFieldFormRef?: React.RefObject<HTMLDivElement>;
pendingFieldsRef?: React.RefObject<HTMLDivElement>;
}
function FieldListItemComponent(
@ -85,6 +87,7 @@ function FieldListItemComponent(
state,
isAddingFields,
setPreviousState,
pendingFieldsRef,
}: Props,
ref: React.Ref<HTMLLIElement>
) {
@ -141,7 +144,6 @@ function FieldListItemComponent(
const { addMultiFieldButtonLabel, addPropertyButtonLabel, editButtonLabel, deleteButtonLabel } =
i18nTexts;
return (
<EuiFlexGroup gutterSize="s" className="mappingsEditor__fieldsListItem__actions">
{canHaveMultiFields && (
@ -321,6 +323,7 @@ function FieldListItemComponent(
state={state}
isAddingFields={isAddingFields}
setPreviousState={setPreviousState}
pendingFieldsRef={pendingFieldsRef}
/>
)}

View file

@ -18,6 +18,7 @@ interface Props {
state: State;
setPreviousState?: (state: State) => void;
isAddingFields?: boolean;
pendingFieldsRef?: React.RefObject<HTMLDivElement>;
}
export const FieldsListItemContainer = ({
@ -27,6 +28,7 @@ export const FieldsListItemContainer = ({
state,
setPreviousState,
isAddingFields,
pendingFieldsRef,
}: Props) => {
const dispatch = useDispatch();
const listElement = useRef<HTMLLIElement | null>(null);
@ -110,6 +112,7 @@ export const FieldsListItemContainer = ({
toggleExpand={toggleExpand}
state={state}
isAddingFields={isAddingFields}
pendingFieldsRef={pendingFieldsRef}
/>
);
};

View file

@ -7,7 +7,7 @@
import { EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useRef } from 'react';
import { useDispatch, useMappingsState } from '../../mappings_state_context';
import { CreateField, FieldsList, SemanticTextInfo } from './fields';
@ -16,19 +16,21 @@ interface Props {
onCancelAddingNewFields?: () => void;
isAddingFields?: boolean;
semanticTextInfo?: SemanticTextInfo;
pendingFieldsRef?: React.RefObject<HTMLDivElement>;
}
export const DocumentFieldsTreeEditor = ({
onCancelAddingNewFields,
isAddingFields,
semanticTextInfo,
pendingFieldsRef,
}: Props) => {
const dispatch = useDispatch();
const {
fields: { byId, rootLevelFields },
documentFields: { status, fieldToAddFieldTo },
} = useMappingsState();
const createFieldFormRef = useRef<HTMLDivElement>(null);
const getField = useCallback((fieldId: string) => byId[fieldId], [byId]);
const fields = useMemo(() => rootLevelFields.map(getField), [rootLevelFields, getField]);
@ -52,6 +54,7 @@ export const DocumentFieldsTreeEditor = ({
onCancelAddingNewFields={onCancelAddingNewFields}
isAddingFields={isAddingFields}
semanticTextInfo={semanticTextInfo}
createFieldFormRef={createFieldFormRef}
/>
);
};
@ -77,7 +80,12 @@ export const DocumentFieldsTreeEditor = ({
return (
<>
<FieldsList fields={fields} state={useMappingsState()} isAddingFields={isAddingFields} />
<FieldsList
fields={fields}
state={useMappingsState()}
isAddingFields={isAddingFields}
pendingFieldsRef={pendingFieldsRef}
/>
{renderCreateField()}
{renderAddFieldButton()}
</>

View file

@ -27,7 +27,7 @@ import {
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react';
import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ILicense } from '@kbn/licensing-plugin/public';
import { useUnsavedChangesPrompt } from '@kbn/unsaved-changes-prompt';
import {
@ -85,6 +85,7 @@ export const DetailsPageMappingsContent: FunctionComponent<{
overlays,
history,
} = useAppContext();
const pendingFieldsRef = useRef<HTMLDivElement>(null);
const [isPlatinumLicense, setIsPlatinumLicense] = useState<boolean>(false);
useEffect(() => {
@ -559,7 +560,7 @@ export const DetailsPageMappingsContent: FunctionComponent<{
</EuiFlexItem>
{errorSavingMappings}
{isAddingFields && (
<EuiFlexItem grow={false}>
<EuiFlexItem grow={false} ref={pendingFieldsRef} tabIndex={0}>
<EuiPanel hasBorder paddingSize="s">
<EuiAccordion
id={pendingFieldListId}
@ -597,11 +598,13 @@ export const DetailsPageMappingsContent: FunctionComponent<{
onCancelAddingNewFields={onCancelAddingNewFields}
isAddingFields={isAddingFields}
semanticTextInfo={semanticTextInfo}
pendingFieldsRef={pendingFieldsRef}
/>
) : (
<DocumentFields
isAddingFields={isAddingFields}
semanticTextInfo={semanticTextInfo}
pendingFieldsRef={pendingFieldsRef}
/>
)}
</EuiPanel>