Skip to content

Commit 0d666a5

Browse files
fix(option): Remove aria-selected='false' in single-selection mode
Remove the aria-selected attribute from unselected options in single selection mode. The multi-selection behavior is unchanged. Motivation: the screen reader NVDA announces 'not selected' on any element that has aria-selected='false', which is disruptive when a user is navigating through a long list of options. The W3 aria best practices example only uses aria-selected='true for single selection', false is implicit: https://www.w3.org/TR/wai-aria-practices/examples/listbox/listbox-collapsible.html
1 parent f889547 commit 0d666a5

File tree

3 files changed

+53
-11
lines changed

3 files changed

+53
-11
lines changed

src/lib/core/option/option.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export const MAT_OPTION_PARENT_COMPONENT =
7272
'[class.mat-option-multiple]': 'multiple',
7373
'[class.mat-active]': 'active',
7474
'[id]': 'id',
75-
'[attr.aria-selected]': 'selected.toString()',
75+
'[attr.aria-selected]': '_getAriaSelected()',
7676
'[attr.aria-disabled]': 'disabled.toString()',
7777
'[class.mat-option-disabled]': 'disabled',
7878
'(click)': '_selectViaInteraction()',
@@ -220,6 +220,16 @@ export class MatOption implements AfterViewChecked, OnDestroy {
220220
}
221221
}
222222

223+
/**
224+
* Gets the `aria-selected` value for the option. We explicitly omit the `aria-selected`
225+
* attribute from single-selection, unselected options. Including the `aria-selected="false"`
226+
* attributes adds a significant amount of noise to screen-reader users without providing useful
227+
* information.
228+
*/
229+
_getAriaSelected(): boolean|null {
230+
return this.selected || (this.multiple ? false : null);
231+
}
232+
223233
/** Returns the correct tabindex for the option depending on disabled state. */
224234
_getTabIndex(): string {
225235
return this.disabled ? '-1' : '0';

src/lib/select/select.spec.ts

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -863,7 +863,7 @@ describe('MatSelect', () => {
863863
describe('for options', () => {
864864
let fixture: ComponentFixture<BasicSelect>;
865865
let trigger: HTMLElement;
866-
let options: NodeListOf<HTMLElement>;
866+
let options: Array<HTMLElement>;
867867

868868
beforeEach(fakeAsync(() => {
869869
fixture = TestBed.createComponent(BasicSelect);
@@ -872,8 +872,7 @@ describe('MatSelect', () => {
872872
trigger.click();
873873
fixture.detectChanges();
874874

875-
options =
876-
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
875+
options = Array.from(overlayContainerElement.querySelectorAll('mat-option'));
877876
}));
878877

879878
it('should set the role of mat-option to option', fakeAsync(() => {
@@ -882,10 +881,9 @@ describe('MatSelect', () => {
882881
expect(options[2].getAttribute('role')).toEqual('option');
883882
}));
884883

885-
it('should set aria-selected on each option', fakeAsync(() => {
886-
expect(options[0].getAttribute('aria-selected')).toEqual('false');
887-
expect(options[1].getAttribute('aria-selected')).toEqual('false');
888-
expect(options[2].getAttribute('aria-selected')).toEqual('false');
884+
it('should set aria-selected on each option for single select', fakeAsync(() => {
885+
expect(options.every(option => !option.hasAttribute('aria-selected'))).toBe(true,
886+
'Expected all unselected single-select options not to have aria-selected set.');
889887

890888
options[1].click();
891889
fixture.detectChanges();
@@ -894,11 +892,44 @@ describe('MatSelect', () => {
894892
fixture.detectChanges();
895893
flush();
896894

897-
expect(options[0].getAttribute('aria-selected')).toEqual('false');
898-
expect(options[1].getAttribute('aria-selected')).toEqual('true');
899-
expect(options[2].getAttribute('aria-selected')).toEqual('false');
895+
expect(options[1].getAttribute('aria-selected')).toEqual('true',
896+
'Expected selected single-select option to have aria-selected="true".');
897+
options.splice(1, 1);
898+
expect(options.every(option => !option.hasAttribute('aria-selected'))).toBe(true,
899+
'Expected all unselected single-select options not to have aria-selected set.');
900900
}));
901901

902+
it('should set aria-selected on each option for multi-select', fakeAsync(() => {
903+
fixture.destroy();
904+
905+
const multiFixture = TestBed.createComponent(MultiSelect);
906+
multiFixture.detectChanges();
907+
908+
trigger = multiFixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
909+
trigger.click();
910+
multiFixture.detectChanges();
911+
912+
options = Array.from(overlayContainerElement.querySelectorAll('mat-option'));
913+
914+
expect(options.every(option => option.hasAttribute('aria-selected') &&
915+
option.getAttribute('aria-selected') === 'false')).toBe(true,
916+
'Expected all unselected multi-select options to have aria-selected="false".');
917+
918+
options[1].click();
919+
multiFixture.detectChanges();
920+
921+
trigger.click();
922+
multiFixture.detectChanges();
923+
flush();
924+
925+
expect(options[1].getAttribute('aria-selected')).toEqual('true',
926+
'Expected selected multi-select option to have aria-selected="true".');
927+
options.splice(1, 1);
928+
expect(options.every(option => option.hasAttribute('aria-selected') &&
929+
option.getAttribute('aria-selected') === 'false')).toBe(true,
930+
'Expected all unselected multi-select options to have aria-selected="false".');
931+
}));
932+
902933
it('should set the tabindex of each option according to disabled state', fakeAsync(() => {
903934
expect(options[0].getAttribute('tabindex')).toEqual('0');
904935
expect(options[1].getAttribute('tabindex')).toEqual('0');

tools/public_api_guard/lib/core.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ export declare class MatOption implements AfterViewChecked, OnDestroy {
244244
value: any;
245245
readonly viewValue: string;
246246
constructor(_element: ElementRef<HTMLElement>, _changeDetectorRef: ChangeDetectorRef, _parent: MatOptionParentComponent, group: MatOptgroup);
247+
_getAriaSelected(): boolean | null;
247248
_getHostElement(): HTMLElement;
248249
_getTabIndex(): string;
249250
_handleKeydown(event: KeyboardEvent): void;

0 commit comments

Comments
 (0)