Skip to content

Commit a3a4b8e

Browse files
feat(option): add Input to remove aria-selected='false'
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 f889547 commit a3a4b8e

File tree

2 files changed

+49
-1
lines changed

2 files changed

+49
-1
lines changed

src/lib/core/option/option.spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,44 @@ describe('MatOption component', () => {
187187
expect(optionNativeElement.querySelectorAll('.mat-ripple-element').length)
188188
.toBe(0, 'Expected no ripples to show up after click on a disabled option.');
189189
});
190+
});
191+
192+
describe('aria attributes', () => {
193+
let fixture: ComponentFixture<BasicOption>;
194+
let optionNativeElement: HTMLElement;
195+
let optionInstance: MatOption;
196+
197+
beforeEach(() => {
198+
fixture = TestBed.createComponent(BasicOption);
199+
fixture.detectChanges();
200+
201+
const optionDebugElement = fixture.debugElement.query(By.directive(MatOption));
202+
optionNativeElement = optionDebugElement.nativeElement;
203+
optionInstance = optionDebugElement.componentInstance;
204+
});
205+
206+
it('should toggle aria-selected between true and false by default', () => {
207+
expect(optionNativeElement.getAttribute('aria-selected')).toBe('false');
208+
optionInstance.select();
209+
fixture.detectChanges();
210+
expect(optionNativeElement.getAttribute('aria-selected')).toBe('true');
211+
optionInstance.deselect();
212+
fixture.detectChanges();
213+
expect(optionNativeElement.getAttribute('aria-selected')).toBe('false');
214+
});
190215

216+
it('should toggle aria-selected between true and null when specified',
217+
() => {
218+
optionInstance.disableAriaSelectedExplicitFalse = true;
219+
fixture.detectChanges();
220+
expect(optionNativeElement.getAttribute('aria-selected')).toBe(null);
221+
optionInstance.select();
222+
fixture.detectChanges();
223+
expect(optionNativeElement.getAttribute('aria-selected')).toBe('true');
224+
optionInstance.deselect();
225+
fixture.detectChanges();
226+
expect(optionNativeElement.getAttribute('aria-selected')).toBe(null);
227+
});
191228
});
192229

193230
});

src/lib/core/option/option.ts

Lines changed: 12 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()',
@@ -102,6 +102,12 @@ export class MatOption implements AfterViewChecked, OnDestroy {
102102
/** The unique ID of the option. */
103103
@Input() id: string = `mat-option-${_uniqueIdCounter++}`;
104104

105+
/**
106+
* Whether to remove the aria-selected attribute when the option is not selected. Default is
107+
* explicitly setting aria-selected='false'.
108+
*/
109+
@Input() disableAriaSelectedExplicitFalse = false;
110+
105111
/** Whether the option is disabled. */
106112
@Input()
107113
get disabled() { return (this.group && this.group.disabled) || this._disabled; }
@@ -220,6 +226,11 @@ export class MatOption implements AfterViewChecked, OnDestroy {
220226
}
221227
}
222228

229+
/** Returns the aria-selected value for the option, which can be 'true', 'false', or null. */
230+
_getAriaSelected(): string|null {
231+
return !this.selected && this.disableAriaSelectedExplicitFalse ? null : this.selected.toString();
232+
}
233+
223234
/** Returns the correct tabindex for the option depending on disabled state. */
224235
_getTabIndex(): string {
225236
return this.disabled ? '-1' : '0';

0 commit comments

Comments
 (0)