Skip to content

Commit 61a608b

Browse files
vanessanschmittjelbourn
authored andcommitted
fix(option): Remove aria-selected='false' in single-selection mode (#15617)
Add an Input to optionally remove the aria-selected attribute from unselected options. The default 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', false is implicit: https://www.w3.org/TR/wai-aria-practices/examples/listbox/listbox-collapsible.html
1 parent 9c34b97 commit 61a608b

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
@@ -848,7 +848,7 @@ describe('MatSelect', () => {
848848
describe('for options', () => {
849849
let fixture: ComponentFixture<BasicSelect>;
850850
let trigger: HTMLElement;
851-
let options: NodeListOf<HTMLElement>;
851+
let options: Array<HTMLElement>;
852852

853853
beforeEach(fakeAsync(() => {
854854
fixture = TestBed.createComponent(BasicSelect);
@@ -857,8 +857,7 @@ describe('MatSelect', () => {
857857
trigger.click();
858858
fixture.detectChanges();
859859

860-
options =
861-
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
860+
options = Array.from(overlayContainerElement.querySelectorAll('mat-option'));
862861
}));
863862

864863
it('should set the role of mat-option to option', fakeAsync(() => {
@@ -867,10 +866,9 @@ describe('MatSelect', () => {
867866
expect(options[2].getAttribute('role')).toEqual('option');
868867
}));
869868

870-
it('should set aria-selected on each option', fakeAsync(() => {
871-
expect(options[0].getAttribute('aria-selected')).toEqual('false');
872-
expect(options[1].getAttribute('aria-selected')).toEqual('false');
873-
expect(options[2].getAttribute('aria-selected')).toEqual('false');
869+
it('should set aria-selected on each option for single select', fakeAsync(() => {
870+
expect(options.every(option => !option.hasAttribute('aria-selected'))).toBe(true,
871+
'Expected all unselected single-select options not to have aria-selected set.');
874872

875873
options[1].click();
876874
fixture.detectChanges();
@@ -879,11 +877,44 @@ describe('MatSelect', () => {
879877
fixture.detectChanges();
880878
flush();
881879

882-
expect(options[0].getAttribute('aria-selected')).toEqual('false');
883-
expect(options[1].getAttribute('aria-selected')).toEqual('true');
884-
expect(options[2].getAttribute('aria-selected')).toEqual('false');
880+
expect(options[1].getAttribute('aria-selected')).toEqual('true',
881+
'Expected selected single-select option to have aria-selected="true".');
882+
options.splice(1, 1);
883+
expect(options.every(option => !option.hasAttribute('aria-selected'))).toBe(true,
884+
'Expected all unselected single-select options not to have aria-selected set.');
885885
}));
886886

887+
it('should set aria-selected on each option for multi-select', fakeAsync(() => {
888+
fixture.destroy();
889+
890+
const multiFixture = TestBed.createComponent(MultiSelect);
891+
multiFixture.detectChanges();
892+
893+
trigger = multiFixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
894+
trigger.click();
895+
multiFixture.detectChanges();
896+
897+
options = Array.from(overlayContainerElement.querySelectorAll('mat-option'));
898+
899+
expect(options.every(option => option.hasAttribute('aria-selected') &&
900+
option.getAttribute('aria-selected') === 'false')).toBe(true,
901+
'Expected all unselected multi-select options to have aria-selected="false".');
902+
903+
options[1].click();
904+
multiFixture.detectChanges();
905+
906+
trigger.click();
907+
multiFixture.detectChanges();
908+
flush();
909+
910+
expect(options[1].getAttribute('aria-selected')).toEqual('true',
911+
'Expected selected multi-select option to have aria-selected="true".');
912+
options.splice(1, 1);
913+
expect(options.every(option => option.hasAttribute('aria-selected') &&
914+
option.getAttribute('aria-selected') === 'false')).toBe(true,
915+
'Expected all unselected multi-select options to have aria-selected="false".');
916+
}));
917+
887918
it('should set the tabindex of each option according to disabled state', fakeAsync(() => {
888919
expect(options[0].getAttribute('tabindex')).toEqual('0');
889920
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)