fix: [Rules > Add/Edit rule exception][AXE-CORE]: Form elements must have an accessible label (#177923)

Closes: https://github.com/elastic/security-team/issues/8572
Closes: https://github.com/elastic/security-team/issues/8573
Closes: https://github.com/elastic/security-team/issues/8613
Closes: https://github.com/elastic/security-team/issues/8614

## Description

The [axe browser plugin](https://deque.com/axe) is reporting two form
elements without labels in the Create endpoint exception flyout.
Screenshot attached below.

### Steps to recreate

1. Open [Shared Exception
Lists](https://kibana.siem.estc.dev/app/security/exceptions)
2. Click the Create Endpoint Exception button
3. Run an axe browser scan in Chrome, Edge, or Firefox
4. Verify the form label missing errors

### Screens 

#### a11y attributes

<img width="1419" alt="image"
src="61ef3ea1-bd65-46e0-9274-ae9aa329a04d">


#### Axe report

<img width="1419" alt="image"
src="df5ba30c-78f5-4418-b3b6-09a028e1c606">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com>
This commit is contained in:
Alexey Antonov 2024-03-06 15:57:42 +02:00 committed by GitHub
parent 8f39000270
commit b63c9e2824
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 124 additions and 33 deletions

View file

@ -28,6 +28,7 @@ export const FieldComponent: React.FC<FieldProps> = ({
selectedField,
acceptsCustomOptions = false,
showMappingConflicts = false,
'aria-label': ariaLabel,
}): JSX.Element => {
const {
isInvalid,
@ -71,6 +72,7 @@ export const FieldComponent: React.FC<FieldProps> = ({
})}
fullWidth
renderOption={renderFields}
aria-label={ariaLabel}
/>
);
}
@ -91,6 +93,7 @@ export const FieldComponent: React.FC<FieldProps> = ({
style={fieldWidth}
fullWidth
renderOption={renderFields}
aria-label={ariaLabel}
/>
);
};

View file

@ -17,6 +17,7 @@ export interface FieldProps extends FieldBaseProps {
placeholder: string;
acceptsCustomOptions?: boolean;
showMappingConflicts?: boolean;
'aria-label'?: string;
}
export interface FieldBaseProps {
indexPattern: DataViewBase | undefined;

View file

@ -14,11 +14,13 @@ const NO_OPTIONS_FOR_EXIST: EuiComboBoxOptionOption[] = [];
interface AutocompleteFieldExistsProps {
placeholder: string;
rowLabel?: string;
'aria-label'?: string;
}
export const AutocompleteFieldExistsComponent: React.FC<AutocompleteFieldExistsProps> = ({
placeholder,
rowLabel,
'aria-label': ariaLabel,
}): JSX.Element => (
<EuiFormRow label={rowLabel} fullWidth>
<EuiComboBox
@ -28,6 +30,7 @@ export const AutocompleteFieldExistsComponent: React.FC<AutocompleteFieldExistsP
onChange={undefined}
isDisabled
data-test-subj="valuesAutocompleteComboBox existsComboxBox"
aria-label={ariaLabel}
fullWidth
/>
</EuiFormRow>

View file

@ -35,6 +35,7 @@ interface AutocompleteFieldListsProps {
selectedField: DataViewFieldBase | undefined;
selectedValue: string | undefined;
allowLargeValueLists?: boolean;
'aria-label'?: string;
}
export interface AutocompleteListsData {
@ -53,6 +54,7 @@ export const AutocompleteFieldListsComponent: React.FC<AutocompleteFieldListsPro
selectedField,
selectedValue,
allowLargeValueLists = false,
'aria-label': ariaLabel,
}): JSX.Element => {
const [error, setError] = useState<string | undefined>(undefined);
const [listData, setListData] = useState<AutocompleteListsData>({
@ -162,6 +164,7 @@ export const AutocompleteFieldListsComponent: React.FC<AutocompleteFieldListsPro
selectedOptions={selectedComboOptions}
singleSelection={SINGLE_SELECTION}
sortMatchesBy="startsWith"
aria-label={ariaLabel}
/>
</EuiFormRow>
);

View file

@ -54,6 +54,7 @@ interface AutocompleteFieldMatchProps {
autocompleteService: AutocompleteStart;
onChange: (arg: string) => void;
onError?: (arg: boolean) => void;
'aria-label'?: string;
}
export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchProps> = ({
@ -70,6 +71,7 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
autocompleteService,
onChange,
onError,
'aria-label': ariaLabel,
}): JSX.Element => {
const [searchQuery, setSearchQuery] = useState('');
const [touched, setIsTouched] = useState(false);
@ -245,27 +247,29 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
sortMatchesBy="startsWith"
data-test-subj="valuesAutocompleteMatch"
style={fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}}
aria-label={ariaLabel}
fullWidth
async
/>
</EuiFormRow>
);
}, [
comboOptions,
error,
fieldInputWidth,
inputPlaceholder,
isClearable,
isDisabled,
isLoadingState,
rowLabel,
selectedComboOptions,
error,
selectedField,
showSpacesWarning,
handleCreateOption,
handleSearchChange,
inputPlaceholder,
isDisabled,
isLoadingState,
isClearable,
comboOptions,
selectedComboOptions,
handleValuesChange,
handleSearchChange,
handleCreateOption,
setIsTouchedValue,
fieldInputWidth,
ariaLabel,
]);
if (!isSuggestingValues && selectedField != null) {
@ -290,6 +294,7 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
onChange={handleNonComboBoxInputChange}
data-test-subj="valueAutocompleteFieldMatchNumber"
style={fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}}
aria-label={ariaLabel}
fullWidth
/>
</EuiFormRow>
@ -310,6 +315,7 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
onChange={handleBooleanInputChange}
data-test-subj="valuesAutocompleteMatchBoolean"
style={fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}}
aria-label={ariaLabel}
fullWidth
/>
</EuiFormRow>

View file

@ -38,6 +38,7 @@ interface AutocompleteFieldMatchAnyProps {
autocompleteService: AutocompleteStart;
onChange: (arg: string[]) => void;
onError?: (arg: boolean) => void;
'aria-label'?: string;
}
export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatchAnyProps> = ({
@ -53,6 +54,7 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
onChange,
onError,
autocompleteService,
'aria-label': ariaLabel,
}): JSX.Element => {
const [searchQuery, setSearchQuery] = useState('');
const [touched, setIsTouched] = useState(false);
@ -187,26 +189,28 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
isCaseSensitive
onBlur={setIsTouchedValue}
data-test-subj="valuesAutocompleteMatchAny"
aria-label={ariaLabel}
fullWidth
async
/>
</EuiFormRow>
);
}, [
comboOptions,
error,
handleCreateOption,
handleSearchChange,
handleValuesChange,
inputPlaceholder,
isClearable,
isDisabled,
isLoadingState,
rowLabel,
selectedComboOptions,
error,
selectedField,
showSpacesWarning,
inputPlaceholder,
isLoadingState,
isClearable,
isDisabled,
comboOptions,
selectedComboOptions,
handleValuesChange,
handleSearchChange,
handleCreateOption,
setIsTouchedValue,
ariaLabel,
]);
if (!isSuggestingValues && selectedField != null) {
@ -232,6 +236,7 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
isInvalid={selectedField != null && error != null}
onFocus={setIsTouchedValue}
data-test-subj="valuesAutocompleteMatchAnyNumber"
aria-label={ariaLabel}
fullWidth
/>
</EuiFormRow>

View file

@ -46,6 +46,7 @@ interface AutocompleteFieldWildcardProps {
onError: (arg: boolean) => void;
onWarning: (arg: boolean) => void;
warning?: Warning;
'aria-label'?: string;
}
export const AutocompleteFieldWildcardComponent: React.FC<AutocompleteFieldWildcardProps> = memo(
@ -65,6 +66,7 @@ export const AutocompleteFieldWildcardComponent: React.FC<AutocompleteFieldWildc
onError,
onWarning,
warning,
'aria-label': ariaLabel,
}): JSX.Element => {
const [searchQuery, setSearchQuery] = useState('');
const [touched, setIsTouched] = useState(false);
@ -252,26 +254,28 @@ export const AutocompleteFieldWildcardComponent: React.FC<AutocompleteFieldWildc
style={fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}}
fullWidth
async
aria-label={ariaLabel}
/>
</EuiFormRow>
);
}, [
comboOptions,
error,
fieldInputWidth,
handleCreateOption,
handleSearchChange,
handleValuesChange,
inputPlaceholder,
isClearable,
isDisabled,
isLoadingState,
rowLabel,
selectedComboOptions,
selectedField,
setIsTouchedValue,
error,
warning,
showSpacesWarning,
selectedField,
inputPlaceholder,
isDisabled,
isLoadingState,
isClearable,
comboOptions,
selectedComboOptions,
handleValuesChange,
handleSearchChange,
handleCreateOption,
setIsTouchedValue,
fieldInputWidth,
ariaLabel,
]);
return defaultInput;

View file

@ -29,6 +29,7 @@ interface OperatorState {
operatorOptions?: OperatorOption[];
placeholder: string;
selectedField: DataViewFieldBase | undefined;
'aria-label'?: string;
}
export const OperatorComponent: React.FC<OperatorState> = ({
@ -41,6 +42,7 @@ export const OperatorComponent: React.FC<OperatorState> = ({
operatorInputWidth = 150,
placeholder,
selectedField,
'aria-label': ariaLabel,
}): JSX.Element => {
const getLabel = useCallback(({ message }): string => message, []);
const optionsMemo = useMemo(
@ -90,6 +92,7 @@ export const OperatorComponent: React.FC<OperatorState> = ({
singleSelection={AS_PLAIN_TEXT}
data-test-subj="operatorAutocompleteComboBox"
style={inputWidth}
aria-label={ariaLabel}
/>
);
};

View file

@ -82,6 +82,7 @@ describe('BuilderEntryItem', () => {
onChange={jest.fn()}
setErrorsExist={jest.fn()}
setWarningsExist={jest.fn()}
exceptionItemIndex={0}
showLabel
/>
);
@ -116,6 +117,7 @@ describe('BuilderEntryItem', () => {
setWarningsExist={jest.fn()}
showLabel
allowCustomOptions
exceptionItemIndex={0}
/>
);
@ -153,6 +155,7 @@ describe('BuilderEntryItem', () => {
setWarningsExist={jest.fn()}
showLabel
allowCustomOptions
exceptionItemIndex={0}
/>
);
@ -188,6 +191,7 @@ describe('BuilderEntryItem', () => {
setWarningsExist={jest.fn()}
showLabel
allowCustomOptions={false}
exceptionItemIndex={0}
/>
);
@ -225,6 +229,7 @@ describe('BuilderEntryItem', () => {
showLabel
allowCustomOptions
getExtendedFields={(): Promise<FieldSpec[]> => Promise.resolve([field])}
exceptionItemIndex={0}
/>
);
@ -261,6 +266,7 @@ describe('BuilderEntryItem', () => {
setErrorsExist={jest.fn()}
setWarningsExist={jest.fn()}
showLabel={false}
exceptionItemIndex={0}
/>
);
@ -300,6 +306,7 @@ describe('BuilderEntryItem', () => {
setErrorsExist={jest.fn()}
setWarningsExist={jest.fn()}
showLabel={false}
exceptionItemIndex={0}
/>
);
@ -339,6 +346,7 @@ describe('BuilderEntryItem', () => {
setErrorsExist={jest.fn()}
setWarningsExist={jest.fn()}
showLabel={false}
exceptionItemIndex={0}
/>
);
@ -378,6 +386,7 @@ describe('BuilderEntryItem', () => {
setErrorsExist={jest.fn()}
setWarningsExist={jest.fn()}
showLabel={false}
exceptionItemIndex={0}
/>
);
@ -418,6 +427,7 @@ describe('BuilderEntryItem', () => {
setErrorsExist={jest.fn()}
setWarningsExist={jest.fn()}
showLabel
exceptionItemIndex={0}
/>
);
@ -459,6 +469,7 @@ describe('BuilderEntryItem', () => {
setErrorsExist={jest.fn()}
setWarningsExist={jest.fn()}
showLabel
exceptionItemIndex={0}
/>
);
@ -499,6 +510,7 @@ describe('BuilderEntryItem', () => {
setErrorsExist={jest.fn()}
setWarningsExist={jest.fn()}
showLabel={false}
exceptionItemIndex={0}
/>
);
@ -542,6 +554,7 @@ describe('BuilderEntryItem', () => {
setErrorsExist={jest.fn()}
setWarningsExist={jest.fn()}
showLabel={false}
exceptionItemIndex={0}
/>
);
@ -585,6 +598,7 @@ describe('BuilderEntryItem', () => {
setErrorsExist={jest.fn()}
setWarningsExist={jest.fn()}
showLabel={false}
exceptionItemIndex={0}
/>
);
@ -628,6 +642,7 @@ describe('BuilderEntryItem', () => {
setErrorsExist={jest.fn()}
setWarningsExist={jest.fn()}
showLabel={false}
exceptionItemIndex={0}
/>
);
@ -662,6 +677,7 @@ describe('BuilderEntryItem', () => {
setErrorsExist={jest.fn()}
setWarningsExist={jest.fn()}
showLabel={false}
exceptionItemIndex={0}
/>
);
@ -721,6 +737,7 @@ describe('BuilderEntryItem', () => {
setErrorsExist={jest.fn()}
setWarningsExist={jest.fn()}
showLabel={false}
exceptionItemIndex={0}
/>
);
@ -764,6 +781,7 @@ describe('BuilderEntryItem', () => {
setErrorsExist={jest.fn()}
setWarningsExist={jest.fn()}
showLabel={false}
exceptionItemIndex={0}
/>
);
@ -805,6 +823,7 @@ describe('BuilderEntryItem', () => {
setErrorsExist={jest.fn()}
setWarningsExist={jest.fn()}
showLabel={false}
exceptionItemIndex={0}
/>
);
@ -846,6 +865,7 @@ describe('BuilderEntryItem', () => {
setErrorsExist={jest.fn()}
setWarningsExist={jest.fn()}
showLabel={false}
exceptionItemIndex={0}
/>
);
@ -887,6 +907,7 @@ describe('BuilderEntryItem', () => {
setErrorsExist={jest.fn()}
setWarningsExist={jest.fn()}
showLabel={false}
exceptionItemIndex={0}
/>
);
@ -928,6 +949,7 @@ describe('BuilderEntryItem', () => {
setErrorsExist={jest.fn()}
setWarningsExist={jest.fn()}
showLabel={false}
exceptionItemIndex={0}
/>
);
@ -975,6 +997,7 @@ describe('BuilderEntryItem', () => {
setErrorsExist={jest.fn()}
setWarningsExist={jest.fn()}
showLabel={false}
exceptionItemIndex={0}
/>
);
@ -1016,6 +1039,7 @@ describe('BuilderEntryItem', () => {
setErrorsExist={mockSetErrorExists}
setWarningsExist={jest.fn()}
showLabel={false}
exceptionItemIndex={0}
/>
);
@ -1056,6 +1080,7 @@ describe('BuilderEntryItem', () => {
setErrorsExist={mockSetErrorExists}
setWarningsExist={jest.fn()}
showLabel={false}
exceptionItemIndex={0}
/>
);
@ -1105,6 +1130,7 @@ describe('BuilderEntryItem', () => {
setErrorsExist={jest.fn()}
setWarningsExist={mockSetWarningsExists}
showLabel={false}
exceptionItemIndex={0}
/>
);
@ -1154,6 +1180,7 @@ describe('BuilderEntryItem', () => {
setErrorsExist={jest.fn()}
setWarningsExist={mockSetWarningsExists}
showLabel={false}
exceptionItemIndex={0}
/>
);
@ -1202,6 +1229,7 @@ describe('BuilderEntryItem', () => {
osTypes={['windows']}
showLabel={false}
isDisabled={true}
exceptionItemIndex={0}
/>
);
expect(

View file

@ -81,6 +81,7 @@ export interface EntryItemProps {
onlyShowListOperators?: boolean;
setErrorsExist: (arg: EntryFieldError) => void;
setWarningsExist: (arg: boolean) => void;
exceptionItemIndex: number;
isDisabled?: boolean;
operatorsList?: OperatorOption[];
allowCustomOptions?: boolean;
@ -104,6 +105,7 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
operatorsList,
allowCustomOptions = false,
getExtendedFields,
exceptionItemIndex,
}): JSX.Element => {
const sPaddingSize = useEuiPaddingSize('s');
@ -209,6 +211,11 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
}
indexPattern={filteredIndexPatterns}
selectedField={entry.field}
aria-label={i18n.EXCEPTION_ITEM_ARIA_LABEL(
i18n.FIELD,
exceptionItemIndex,
entry.entryIndex
)}
isClearable={false}
isLoading={false}
isDisabled={isDisabled || indexPattern == null}
@ -301,6 +308,7 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
[
indexPattern,
entry,
exceptionItemIndex,
isDisabled,
handleFieldChange,
allowCustomOptions,
@ -340,6 +348,11 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
isLoading={false}
isClearable={false}
onChange={handleOperatorChange}
aria-label={i18n.EXCEPTION_ITEM_ARIA_LABEL(
i18n.OPERATOR,
exceptionItemIndex,
entry.entryIndex
)}
data-test-subj="exceptionBuilderEntryOperator"
/>
);
@ -380,6 +393,12 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
// eslint-disable-next-line complexity
const getFieldValueComboBox = (type: OperatorTypeEnum, isFirst: boolean): JSX.Element => {
const ariaLabel = i18n.EXCEPTION_ITEM_ARIA_LABEL(
i18n.VALUE,
exceptionItemIndex,
entry.entryIndex
);
switch (type) {
case OperatorTypeEnum.MATCH:
const value = typeof entry.value === 'string' ? entry.value : undefined;
@ -398,6 +417,7 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
onChange={handleFieldMatchValueChange}
isRequired
data-test-subj="exceptionBuilderEntryFieldMatch"
aria-label={ariaLabel}
/>
);
case OperatorTypeEnum.MATCH_ANY:
@ -420,6 +440,7 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
onError={handleError}
onChange={handleFieldMatchAnyValueChange}
isRequired
aria-label={ariaLabel}
data-test-subj="exceptionBuilderEntryFieldMatchAny"
/>
);
@ -459,6 +480,7 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
rowLabel={isFirst ? i18n.VALUE : undefined}
selectedField={entry.correspondingKeywordField ?? entry.field}
selectedValue={wildcardValue}
aria-label={ariaLabel}
/>
);
case OperatorTypeEnum.LIST:
@ -476,6 +498,7 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
onChange={handleFieldListValueChange}
data-test-subj="exceptionBuilderEntryFieldList"
allowLargeValueLists={allowLargeValueLists}
aria-label={ariaLabel}
/>
);
case OperatorTypeEnum.EXISTS:
@ -484,6 +507,7 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
rowLabel={isFirst ? i18n.VALUE : undefined}
placeholder={getEmptyValue()}
data-test-subj="exceptionBuilderEntryFieldExists"
aria-label={ariaLabel}
/>
);
default:

View file

@ -158,6 +158,7 @@ export const BuilderExceptionListItemComponent = React.memo<BuilderExceptionList
operatorsList={operatorsList}
allowCustomOptions={allowCustomOptions}
getExtendedFields={getExtendedFields}
exceptionItemIndex={exceptionItemIndex}
/>
</MyOverflowContainer>
<BuilderEntryDeleteButtonComponent

View file

@ -19,6 +19,16 @@ export const VALUE = i18n.translate('xpack.lists.exceptions.builder.valueLabel',
defaultMessage: 'Value',
});
export const EXCEPTION_ITEM_ARIA_LABEL = (
name: string,
groupIndex: number,
positionIndex: number
): string =>
i18n.translate('xpack.lists.exceptions.item.ariaLabel', {
defaultMessage: '"{name}" in group {group}, position {position} ',
values: { group: groupIndex + 1, name, position: positionIndex + 1 },
});
export const EXCEPTION_FIELD_VALUE_PLACEHOLDER = i18n.translate(
'xpack.lists.exceptions.builder.exceptionFieldValuePlaceholder',
{