Skip to content

Commit c31f13a

Browse files
committed
fix(material/core): don't remove aria-selected from deselected options
For mat-option, set `aria-selected="false"` on deselected options that are selectable. Conforms 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 options that are selectable. Fix issue where voiceover reads every option as "selected" (#25736). Fix #25736
1 parent 147a354 commit c31f13a

File tree

5 files changed

+34
-21
lines changed

5 files changed

+34
-21
lines changed

src/material/core/option/option.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -195,16 +195,6 @@ export class _MatOptionBase<T = any> implements FocusableOption, AfterViewChecke
195195
}
196196
}
197197

198-
/**
199-
* Gets the `aria-selected` value for the option. We explicitly omit the `aria-selected`
200-
* attribute from single-selection, unselected options. Including the `aria-selected="false"`
201-
* attributes adds a significant amount of noise to screen-reader users without providing useful
202-
* information.
203-
*/
204-
_getAriaSelected(): boolean | null {
205-
return this.selected || (this.multiple ? false : null);
206-
}
207-
208198
/** Returns the correct tabindex for the option depending on disabled state. */
209199
_getTabIndex(): string {
210200
return this.disabled ? '-1' : '0';
@@ -255,7 +245,18 @@ export class _MatOptionBase<T = any> implements FocusableOption, AfterViewChecke
255245
'[class.mat-mdc-option-active]': 'active',
256246
'[class.mdc-list-item--disabled]': 'disabled',
257247
'[id]': 'id',
258-
'[attr.aria-selected]': '_getAriaSelected()',
248+
// The aria-selected attribute applied to the option conforms to WAI ARIA best practices for
249+
// listbox interaction pattern.
250+
//
251+
// From [WAI ARIA Listbox authoring practices guide](
252+
// https://www.w3.org/WAI/ARIA/apg/patterns/listbox/):
253+
// "If any options are selected, each selected option has either aria-selected or aria-checked
254+
// set to true. All options that are selectable but not selected have either aria-selected or
255+
// aria-checked set to false."
256+
//
257+
// Set `aria-selected="false"` on not-selected listbox options that are selectable to fix
258+
// VoiceOver reading every option as "selected" (#25736).
259+
'[attr.aria-selected]': 'selected',
259260
'[attr.aria-disabled]': 'disabled.toString()',
260261
'(click)': '_selectViaInteraction()',
261262
'(keydown)': '_handleKeydown($event)',

src/material/legacy-core/option/option.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,19 @@ import {MatLegacyOptgroup} from './optgroup';
2727
* Single option inside of a `<mat-select>` element.
2828
* @deprecated Use `MatOption` from `@angular/material/core` instead. See https://material.angular.io/guide/mdc-migration for information about migrating.
2929
* @breaking-change 17.0.0
30+
*
31+
* The aria-selected attribute applied to the option conforms to WAI ARIA best practices for listbox
32+
* interaction patterns.
33+
*
34+
* From [WAI ARIA Listbox authoring practices guide](
35+
* https://www.w3.org/WAI/ARIA/apg/patterns/listbox/):
36+
* "If any options are selected, each selected option has either aria-selected or aria-checked
37+
* set to true. All options that are selectable but not selected have either aria-selected or
38+
* aria-checked set to false."
39+
*
40+
* Set `aria-selected="false"` on not-selected listbox options that are selectable to fix
41+
* VoiceOver reading every option as "selected" (#25736). Also fixes chromevox not announcing
42+
* options as selectable.
3043
*/
3144
@Component({
3245
selector: 'mat-option',
@@ -38,7 +51,7 @@ import {MatLegacyOptgroup} from './optgroup';
3851
'[class.mat-option-multiple]': 'multiple',
3952
'[class.mat-active]': 'active',
4053
'[id]': 'id',
41-
'[attr.aria-selected]': '_getAriaSelected()',
54+
'[attr.aria-selected]': 'selected',
4255
'[attr.aria-disabled]': 'disabled.toString()',
4356
'[class.mat-option-disabled]': 'disabled',
4457
'(click)': '_selectViaInteraction()',

src/material/legacy-select/select.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,9 +1135,9 @@ describe('MatSelect', () => {
11351135
}));
11361136

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

@@ -1152,9 +1152,9 @@ describe('MatSelect', () => {
11521152
.withContext('Expected selected single-select option to have aria-selected="true".')
11531153
.toEqual('true');
11541154
options.splice(1, 1);
1155-
expect(options.every(option => !option.hasAttribute('aria-selected')))
1155+
expect(options.every(option => option.getAttribute('aria-selected') === 'false'))
11561156
.withContext(
1157-
'Expected all unselected single-select options not to have ' + 'aria-selected set.',
1157+
'Expected all unselected single-select options to have aria-selected="false" set.',
11581158
)
11591159
.toBe(true);
11601160
}));

src/material/select/select.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1168,9 +1168,9 @@ describe('MDC-based MatSelect', () => {
11681168
}));
11691169

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

@@ -1187,9 +1187,9 @@ describe('MDC-based MatSelect', () => {
11871187
)
11881188
.toEqual('true');
11891189
options.splice(1, 1);
1190-
expect(options.every(option => !option.hasAttribute('aria-selected')))
1190+
expect(options.every(option => option.getAttribute('aria-selected') === 'false'))
11911191
.withContext(
1192-
'Expected all unselected single-select options not to have ' + 'aria-selected set.',
1192+
'Expected all unselected single-select options to have aria-selected="false" set.',
11931193
)
11941194
.toBe(true);
11951195
}));

tools/public_api_guard/material/core.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,6 @@ export class _MatOptionBase<T = any> implements FocusableOption, AfterViewChecke
280280
set disabled(value: BooleanInput);
281281
get disableRipple(): boolean;
282282
focus(_origin?: FocusOrigin, options?: FocusOptions): void;
283-
_getAriaSelected(): boolean | null;
284283
_getHostElement(): HTMLElement;
285284
getLabel(): string;
286285
_getTabIndex(): string;

0 commit comments

Comments
 (0)