Skip to content

Commit

Permalink
fix(material/core): mat-option sets aria-selected="false" (#26673)
Browse files Browse the repository at this point in the history
For mat-option, set `aria-selected="false"` on deselected options.
Confirms with [WAI ARIA Listbox authoring practices
guide](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/), which says to
always include aria-selected attribute on listbox options that can be
selected. Fix issue where VoiceOver reads every option as "selected"
(21491).

Fix #21491

(cherry picked from commit 4c8f505)
  • Loading branch information
zarend committed Mar 7, 2023
1 parent e1c8fe8 commit 1bf2fc2
Show file tree
Hide file tree
Showing 5 changed files with 19 additions and 21 deletions.
21 changes: 10 additions & 11 deletions src/material/core/option/option.ts
Expand Up @@ -204,16 +204,6 @@ export class _MatOptionBase<T = any> implements FocusableOption, AfterViewChecke
}
}

/**
* Gets the `aria-selected` value for the option. We explicitly omit the `aria-selected`
* attribute from single-selection, unselected options. Including the `aria-selected="false"`
* attributes adds a significant amount of noise to screen-reader users without providing useful
* information.
*/
_getAriaSelected(): boolean | null {
return this.selected || (this.multiple ? false : null);
}

/** Returns the correct tabindex for the option depending on disabled state. */
_getTabIndex(): string {
return this.disabled ? '-1' : '0';
Expand Down Expand Up @@ -267,7 +257,16 @@ export class _MatOptionBase<T = any> implements FocusableOption, AfterViewChecke
'[class.mat-mdc-option-active]': 'active',
'[class.mdc-list-item--disabled]': 'disabled',
'[id]': 'id',
'[attr.aria-selected]': '_getAriaSelected()',
// Set aria-selected to false for non-selected items and true for selected items. Conform to
// [WAI ARIA Listbox authoring practices guide](
// https://www.w3.org/WAI/ARIA/apg/patterns/listbox/), "If any options are selected, each
// selected option has either aria-selected or aria-checked set to true. All options that are
// selectable but not selected have either aria-selected or aria-checked set to false." Align
// aria-selected implementation of Chips and List components.
//
// Set `aria-selected="false"` on not-selected listbox options to fix VoiceOver announcing
// every option as "selected" (#21491).
'[attr.aria-selected]': 'selected',
'[attr.aria-disabled]': 'disabled.toString()',
'(click)': '_selectViaInteraction()',
'(keydown)': '_handleKeydown($event)',
Expand Down
2 changes: 1 addition & 1 deletion src/material/legacy-core/option/option.ts
Expand Up @@ -38,7 +38,7 @@ import {MatLegacyOptgroup} from './optgroup';
'[class.mat-option-multiple]': 'multiple',
'[class.mat-active]': 'active',
'[id]': 'id',
'[attr.aria-selected]': '_getAriaSelected()',
'[attr.aria-selected]': 'selected',
'[attr.aria-disabled]': 'disabled.toString()',
'[class.mat-option-disabled]': 'disabled',
'(click)': '_selectViaInteraction()',
Expand Down
8 changes: 4 additions & 4 deletions src/material/legacy-select/select.spec.ts
Expand Up @@ -1151,9 +1151,9 @@ describe('MatSelect', () => {
}));

it('should set aria-selected on each option for single select', fakeAsync(() => {
expect(options.every(option => !option.hasAttribute('aria-selected')))
expect(options.every(option => option.getAttribute('aria-selected') === 'false'))
.withContext(
'Expected all unselected single-select options not to have ' + 'aria-selected set.',
'Expected all unselected single-select options to have aria-selected="false".',
)
.toBe(true);

Expand All @@ -1168,9 +1168,9 @@ describe('MatSelect', () => {
.withContext('Expected selected single-select option to have aria-selected="true".')
.toEqual('true');
options.splice(1, 1);
expect(options.every(option => !option.hasAttribute('aria-selected')))
expect(options.every(option => option.getAttribute('aria-selected') === 'false'))
.withContext(
'Expected all unselected single-select options not to have ' + 'aria-selected set.',
'Expected all unselected single-select options to have aria-selected="false".',
)
.toBe(true);
}));
Expand Down
8 changes: 4 additions & 4 deletions src/material/select/select.spec.ts
Expand Up @@ -1184,9 +1184,9 @@ describe('MDC-based MatSelect', () => {
}));

it('should set aria-selected on each option for single select', fakeAsync(() => {
expect(options.every(option => !option.hasAttribute('aria-selected')))
expect(options.every(option => option.getAttribute('aria-selected') === 'false'))
.withContext(
'Expected all unselected single-select options not to have ' + 'aria-selected set.',
'Expected all unselected single-select options to have ' + 'aria-selected="false".',
)
.toBe(true);

Expand All @@ -1203,9 +1203,9 @@ describe('MDC-based MatSelect', () => {
)
.toEqual('true');
options.splice(1, 1);
expect(options.every(option => !option.hasAttribute('aria-selected')))
expect(options.every(option => option.getAttribute('aria-selected') === 'false'))
.withContext(
'Expected all unselected single-select options not to have ' + 'aria-selected set.',
'Expected all unselected single-select options to have ' + 'aria-selected="false".',
)
.toBe(true);
}));
Expand Down
1 change: 0 additions & 1 deletion tools/public_api_guard/material/core.md
Expand Up @@ -279,7 +279,6 @@ export class _MatOptionBase<T = any> implements FocusableOption, AfterViewChecke
set disabled(value: BooleanInput);
get disableRipple(): boolean;
focus(_origin?: FocusOrigin, options?: FocusOptions): void;
_getAriaSelected(): boolean | null;
_getHostElement(): HTMLElement;
getLabel(): string;
_getTabIndex(): string;
Expand Down

0 comments on commit 1bf2fc2

Please sign in to comment.