diff --git a/src-docs/src/views/selectable/data.ts b/src-docs/src/views/selectable/data.ts index 85b0bda4ad9..67f425d3394 100644 --- a/src-docs/src/views/selectable/data.ts +++ b/src-docs/src/views/selectable/data.ts @@ -15,7 +15,6 @@ export const Options: EuiSelectableOption[] = [ }, { label: 'Dione', - id: 'id_dione', }, { label: 'Iapetus', diff --git a/src-docs/src/views/selectable/selectable.tsx b/src-docs/src/views/selectable/selectable.tsx index 29993c2bb17..75977051b5b 100644 --- a/src-docs/src/views/selectable/selectable.tsx +++ b/src-docs/src/views/selectable/selectable.tsx @@ -8,6 +8,7 @@ export default () => { return ( setOptions(newOptions)}> diff --git a/src-docs/src/views/selectable/selectable_custom_render.js b/src-docs/src/views/selectable/selectable_custom_render.js index db103ce7f90..230a1ea86a7 100644 --- a/src-docs/src/views/selectable/selectable_custom_render.js +++ b/src-docs/src/views/selectable/selectable_custom_render.js @@ -84,6 +84,7 @@ export default class extends Component { , snippet: ` this.onChange(options)} listProps={{ bordered: true }}> @@ -141,6 +142,7 @@ export const SelectableExample = { props: { EuiSelectable }, demo: , snippet: `, snippet: ` - {list => list} - + aria-label="Single selection example" + options={options} + onChange={this.onChange} + singleSelection={true} + listProps={{ bordered: true }}> + {list => list} + `, }, { @@ -255,6 +258,7 @@ export const SelectableExample = { props: { EuiSelectable }, demo: , snippet: ` this.onChange(options)}> diff --git a/src-docs/src/views/selectable/selectable_exclusion.tsx b/src-docs/src/views/selectable/selectable_exclusion.tsx index 1a4c87d27b1..48614971c55 100644 --- a/src-docs/src/views/selectable/selectable_exclusion.tsx +++ b/src-docs/src/views/selectable/selectable_exclusion.tsx @@ -8,6 +8,7 @@ export default () => { return ( setOptions(newOptions)}> diff --git a/src-docs/src/views/selectable/selectable_messages.tsx b/src-docs/src/views/selectable/selectable_messages.tsx index 95ec006c00a..26da0f56b65 100644 --- a/src-docs/src/views/selectable/selectable_messages.tsx +++ b/src-docs/src/views/selectable/selectable_messages.tsx @@ -30,7 +30,11 @@ export default () => { checked={isLoading} /> - + {list => (useCustomMessage && !isLoading ? customMessage : list)} diff --git a/src-docs/src/views/selectable/selectable_popover.js b/src-docs/src/views/selectable/selectable_popover.js index a87f2df99cc..0f4d2822a4e 100644 --- a/src-docs/src/views/selectable/selectable_popover.js +++ b/src-docs/src/views/selectable/selectable_popover.js @@ -122,6 +122,7 @@ export default class extends Component { onClose={this.closeFlyout} aria-labelledby="flyoutTitle"> {}} style={{ width: 300 }} diff --git a/src-docs/src/views/selectable/selectable_search.tsx b/src-docs/src/views/selectable/selectable_search.tsx index 4566e56ab71..6649a7ac9cd 100644 --- a/src-docs/src/views/selectable/selectable_search.tsx +++ b/src-docs/src/views/selectable/selectable_search.tsx @@ -9,6 +9,7 @@ export default () => { return ( { return ( setOptions(newOptions)} singleSelection={true} diff --git a/src/components/selectable/__snapshots__/selectable.test.tsx.snap b/src/components/selectable/__snapshots__/selectable.test.tsx.snap index ec2b5e77f42..7bbd9e21b00 100644 --- a/src/components/selectable/__snapshots__/selectable.test.tsx.snap +++ b/src/components/selectable/__snapshots__/selectable.test.tsx.snap @@ -2,7 +2,6 @@ exports[`EuiSelectable is rendered 1`] = `
diff --git a/src/components/selectable/selectable.test.tsx b/src/components/selectable/selectable.test.tsx index 0a886af1047..9a16c3f7889 100644 --- a/src/components/selectable/selectable.test.tsx +++ b/src/components/selectable/selectable.test.tsx @@ -19,6 +19,11 @@ const options: EuiSelectableOption[] = [ }, ]; +// Mock the htmlIdGenerator to generate predictable ids for snapshot tests +jest.mock('../../services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => 'htmlId', +})); + describe('EuiSelectable', () => { test('is rendered', () => { const component = render( diff --git a/src/components/selectable/selectable.tsx b/src/components/selectable/selectable.tsx index e9ad28f45c2..c752a528e6d 100644 --- a/src/components/selectable/selectable.tsx +++ b/src/components/selectable/selectable.tsx @@ -14,10 +14,11 @@ import { EuiSelectableMessage } from './selectable_message'; import { EuiSelectableList } from './selectable_list'; import { EuiLoadingChart } from '../loading'; import { getMatchingOptions } from './matching_options'; -import { comboBoxKeyCodes } from '../../services'; +import { comboBoxKeyCodes, htmlIdGenerator } from '../../services'; import { EuiI18n } from '../i18n'; import { EuiSelectableOption } from './selectable_option'; import { EuiSelectableOptionsListProps } from './selectable_list/selectable_list'; +import { EuiSelectableSearchProps } from './selectable_search/selectable_search'; type RequiredEuiSelectableOptionsListProps = Omit< EuiSelectableOptionsListProps, @@ -43,7 +44,7 @@ type EuiSelectableSearchableProps = ExclusiveUnion< /** * Passes props down to the `EuiFieldSearch` */ - searchProps?: {}; + searchProps?: Partial; } >; @@ -120,7 +121,7 @@ export class EuiSelectable extends Component< }; private optionsListRef = createRef(); - + rootId = htmlIdGenerator(); constructor(props: EuiSelectableProps) { super(props); @@ -302,11 +303,31 @@ export class EuiSelectable extends Component< renderOption, height, allowExclusions, + 'aria-label': ariaLabel, + 'aria-describedby': ariaDescribedby, ...rest } = this.props; const { searchValue, visibleOptions, activeOptionIndex } = this.state; + // Some messy destructuring here to remove aria-label/describedby from searchProps and listProps + // Made messier by some TS requirements + // The aria attributes are then used in getAccessibleName() to place them where they need to go + const unknownAccessibleName = { + 'aria-label': undefined, + 'aria-describedby': undefined, + }; + const { + 'aria-label': searchAriaLabel, + 'aria-describedby': searchAriaDescribedby, + ...cleanedSearchProps + } = searchProps || unknownAccessibleName; + const { + 'aria-label': listAriaLabel, + 'aria-describedby': listAriaDescribedby, + ...cleanedListProps + } = listProps || unknownAccessibleName; + let messageContent; if (isLoading) { @@ -351,37 +372,110 @@ export class EuiSelectable extends Component< className ); + const listId = this.rootId('listbox'); + const makeOptionId = (index: number | undefined) => { + if (typeof index === 'undefined') { + return ''; + } + + return `${listId}_option-${index}`; + }; + + /** + * There are lots of ways to add an accessible name + * Usually we want the same name for the input and the listbox (which is added by aria-label/describedby) + * But you can always override it using searchProps or listProps + * This finds the correct name to use + * + * TODO: This doesn't handle being labelled (