diff --git a/packages/eslint-plugin/changelogs/upcoming/9436.md b/packages/eslint-plugin/changelogs/upcoming/9436.md new file mode 100644 index 000000000000..35c8e9de2d84 --- /dev/null +++ b/packages/eslint-plugin/changelogs/upcoming/9436.md @@ -0,0 +1 @@ +- Updated `no-unnamed-interactive-element` to include checking `EuiColorPicker` \ No newline at end of file diff --git a/packages/eslint-plugin/src/rules/a11y/no_unnamed_interactive_element.test.ts b/packages/eslint-plugin/src/rules/a11y/no_unnamed_interactive_element.test.ts index 2588150fe624..30428956d051 100644 --- a/packages/eslint-plugin/src/rules/a11y/no_unnamed_interactive_element.test.ts +++ b/packages/eslint-plugin/src/rules/a11y/no_unnamed_interactive_element.test.ts @@ -44,6 +44,10 @@ ruleTester.run('NoUnnamedInteractiveElement', NoUnnamedInteractiveElement, { code: '', languageOptions, }, + { + code: '', + languageOptions, + }, // Wrapped in EuiFormRow with label { code: '', @@ -53,6 +57,10 @@ ruleTester.run('NoUnnamedInteractiveElement', NoUnnamedInteractiveElement, { code: '', languageOptions, }, + { + code: '', + languageOptions, + }, ], invalid: [ // Missing a11y prop for interactive components @@ -101,6 +109,11 @@ ruleTester.run('NoUnnamedInteractiveElement', NoUnnamedInteractiveElement, { languageOptions, errors: [{ messageId: 'missingA11y' }], }, + { + code: '', + languageOptions, + errors: [{ messageId: 'missingA11y' }], + }, // Wrapped but missing label { code: '', @@ -112,5 +125,10 @@ ruleTester.run('NoUnnamedInteractiveElement', NoUnnamedInteractiveElement, { languageOptions, errors: [{ messageId: 'missingA11y' }], }, + { + code: '', + languageOptions, + errors: [{ messageId: 'missingA11y' }], + }, ], }); diff --git a/packages/eslint-plugin/src/rules/a11y/no_unnamed_interactive_element.ts b/packages/eslint-plugin/src/rules/a11y/no_unnamed_interactive_element.ts index c6425bd17d3c..a04f19c7d88c 100644 --- a/packages/eslint-plugin/src/rules/a11y/no_unnamed_interactive_element.ts +++ b/packages/eslint-plugin/src/rules/a11y/no_unnamed_interactive_element.ts @@ -24,6 +24,7 @@ const interactiveComponents = [ 'EuiPagination', 'EuiTreeView', 'EuiBreadcrumbs', + 'EuiColorPicker', ] as const; const wrappingComponents = ['EuiFormRow'] as const; @@ -54,7 +55,10 @@ export const NoUnnamedInteractiveElement = ESLintUtils.RuleCreator.withoutDocs({ function report(opening: TSESTree.JSXOpeningElement) { if (opening.name.type !== 'JSXIdentifier') return; const component = opening.name.name; - const allowed = getAllowedA11yPropNamesForComponent(component, a11yConfig).join(', '); + const allowed = getAllowedA11yPropNamesForComponent( + component, + a11yConfig + ).join(', '); context.report({ node: opening, messageId: 'missingA11y', diff --git a/packages/eui/changelogs/upcoming/9436.md b/packages/eui/changelogs/upcoming/9436.md new file mode 100644 index 000000000000..a892c0642bcb --- /dev/null +++ b/packages/eui/changelogs/upcoming/9436.md @@ -0,0 +1,3 @@ +**Accessibility** + +- Fixed `aria-label` not being applied to `EuiColorPicker`'s input element \ No newline at end of file diff --git a/packages/eui/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap b/packages/eui/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap index 3d4ffc901d90..1a18fb64effb 100644 --- a/packages/eui/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap +++ b/packages/eui/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap @@ -26,7 +26,8 @@ exports[`EuiColorPicker color empty string 1`] = ` + + + + Press the down key to open a popover containing color options + +
@@ -78,7 +95,8 @@ exports[`EuiColorPicker color null 1`] = `
+ + + + Press the down key to open a popover containing color options + +
@@ -130,13 +164,30 @@ exports[`EuiColorPicker color valid string 1`] = `
+ + + + Press the down key to open a popover containing color options + +
@@ -181,13 +232,30 @@ exports[`EuiColorPicker compressed 1`] = `
+ + + + Press the down key to open a popover containing color options + +
@@ -232,7 +300,8 @@ exports[`EuiColorPicker disabled 1`] = `
+ + + + Press the down key to open a popover containing color options + + @@ -271,13 +356,30 @@ exports[`EuiColorPicker fullWidth 1`] = ` + + + + Press the down key to open a popover containing color options + +
@@ -531,13 +633,30 @@ exports[`EuiColorPicker isClearable 1`] = `
+ + + + Press the down key to open a popover containing color options + +
@@ -592,7 +711,8 @@ exports[`EuiColorPicker placeholder 1`] = `
+ + + + Press the down key to open a popover containing color options + +
@@ -661,13 +797,30 @@ exports[`EuiColorPicker prepend and append 1`] = `
+ + + + Press the down key to open a popover containing color options + +
@@ -729,7 +882,8 @@ exports[`EuiColorPicker readOnly 1`] = `
+ + + + Press the down key to open a popover containing color options + + @@ -768,13 +938,30 @@ exports[`EuiColorPicker renders 1`] = ` + + + + Press the down key to open a popover containing color options + +
@@ -819,13 +1006,30 @@ exports[`EuiColorPicker showAlpha 1`] = `
+ + + + Press the down key to open a popover containing color options + +
diff --git a/packages/eui/src/components/color_picker/color_picker.stories.tsx b/packages/eui/src/components/color_picker/color_picker.stories.tsx index e7349adc7ecb..3d1fe847c3ae 100644 --- a/packages/eui/src/components/color_picker/color_picker.stories.tsx +++ b/packages/eui/src/components/color_picker/color_picker.stories.tsx @@ -10,7 +10,8 @@ import React, { FunctionComponent, useState, useEffect } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { enableFunctionToggleControls } from '../../../.storybook/utils'; -import { euiPaletteColorBlind } from '../..//services'; +import { euiPaletteColorBlind } from '../../services'; +import { EuiFormRow } from '../form'; import { EuiColorPicker, EuiColorPickerProps } from './color_picker'; const meta: Meta = { @@ -45,6 +46,22 @@ export const Playground: Story = { render: (args) => , }; +export const InFormRow: Story = { + name: 'In FormRow', + parameters: { + loki: { + // The visual composition of label + select is tested by form controls separately + skip: true, + }, + }, + ...Playground, + render: (args) => ( + + + + ), +}; + export const InlineWithAllElements: Story = { tags: ['vrt-only'], args: { diff --git a/packages/eui/src/components/color_picker/color_picker.tsx b/packages/eui/src/components/color_picker/color_picker.tsx index e5babd1c12de..58868be0c5e9 100644 --- a/packages/eui/src/components/color_picker/color_picker.tsx +++ b/packages/eui/src/components/color_picker/color_picker.tsx @@ -23,6 +23,7 @@ import { useEuiMemoizedStyles, keys, useEuiPaletteColorBlind, + useGeneratedHtmlId, } from '../../services'; import { CommonProps } from '../common'; import { @@ -209,6 +210,9 @@ export const EuiColorPicker: FunctionComponent = ({ isClearable = false, placeholder, 'data-test-subj': dataTestSubj, + 'aria-label': _ariaLabel, + 'aria-labelledby': ariaLabelledby, + 'aria-describedby': ariaDescribedby, }) => { const [ popoverLabel, @@ -219,6 +223,7 @@ export const EuiColorPicker: FunctionComponent = ({ alphaLabel, openLabel, closeLabel, + ariaLabel, ] = useEuiI18n( [ 'euiColorPicker.popoverLabel', @@ -229,6 +234,7 @@ export const EuiColorPicker: FunctionComponent = ({ 'euiColorPicker.alphaLabel', 'euiColorPicker.openLabel', 'euiColorPicker.closeLabel', + 'euiColorPicker.ariaLabel', ], [ 'Color selection dialog', @@ -239,9 +245,19 @@ export const EuiColorPicker: FunctionComponent = ({ 'Alpha channel (opacity) value', 'Press the escape key to close the popover', 'Press the down key to open a popover containing color options', + 'Select a color', ] ); + const openLabelId = useGeneratedHtmlId({ + prefix: 'colorPicker', + suffix: 'openLabel', + }); + const closeLabelId = useGeneratedHtmlId({ + prefix: 'colorPicker', + suffix: 'closeLabel', + }); + const defaultSwatches = useEuiPaletteColorBlind(); const swatches = _swatches ?? defaultSwatches; @@ -621,9 +637,35 @@ export const EuiColorPicker: FunctionComponent = ({ fullWidth={fullWidth} autoComplete="off" data-test-subj={testSubjAnchor} - aria-label={isColorSelectorShown ? openLabel : closeLabel} + // if an id is provided it might be used in combination with `htmlFor` on a label, + // so we don't want to override it with a fallback `aria-label` + aria-label={ + _ariaLabel + ? _ariaLabel + : id || ariaLabelledby + ? undefined + : ariaLabel + } + aria-labelledby={ariaLabelledby} + aria-describedby={classNames( + isColorSelectorShown ? openLabelId : closeLabelId, + ariaDescribedby + )} controlOnly // Don't need two EuiFormControlwrappers /> + + + + {/* Separate hint messages that are toggled on the id work more + reliably to prevent stale messages in VO/Safari */} + + {openLabel} + + + {closeLabel} + + + ); }