mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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
This commit is contained in:
committed by
mmalerba
parent
4b93a6e2ef
commit
0f569ff40f
@@ -72,7 +72,7 @@ export const MAT_OPTION_PARENT_COMPONENT =
|
||||
'[class.mat-option-multiple]': 'multiple',
|
||||
'[class.mat-active]': 'active',
|
||||
'[id]': 'id',
|
||||
'[attr.aria-selected]': 'selected.toString()',
|
||||
'[attr.aria-selected]': '_getAriaSelected()',
|
||||
'[attr.aria-disabled]': 'disabled.toString()',
|
||||
'[class.mat-option-disabled]': 'disabled',
|
||||
'(click)': '_selectViaInteraction()',
|
||||
@@ -220,6 +220,16 @@ export class MatOption implements AfterViewChecked, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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';
|
||||
|
||||
@@ -863,7 +863,7 @@ describe('MatSelect', () => {
|
||||
describe('for options', () => {
|
||||
let fixture: ComponentFixture<BasicSelect>;
|
||||
let trigger: HTMLElement;
|
||||
let options: NodeListOf<HTMLElement>;
|
||||
let options: Array<HTMLElement>;
|
||||
|
||||
beforeEach(fakeAsync(() => {
|
||||
fixture = TestBed.createComponent(BasicSelect);
|
||||
@@ -872,8 +872,7 @@ describe('MatSelect', () => {
|
||||
trigger.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
options =
|
||||
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
|
||||
options = Array.from(overlayContainerElement.querySelectorAll('mat-option'));
|
||||
}));
|
||||
|
||||
it('should set the role of mat-option to option', fakeAsync(() => {
|
||||
@@ -882,10 +881,9 @@ describe('MatSelect', () => {
|
||||
expect(options[2].getAttribute('role')).toEqual('option');
|
||||
}));
|
||||
|
||||
it('should set aria-selected on each option', fakeAsync(() => {
|
||||
expect(options[0].getAttribute('aria-selected')).toEqual('false');
|
||||
expect(options[1].getAttribute('aria-selected')).toEqual('false');
|
||||
expect(options[2].getAttribute('aria-selected')).toEqual('false');
|
||||
it('should set aria-selected on each option for single select', fakeAsync(() => {
|
||||
expect(options.every(option => !option.hasAttribute('aria-selected'))).toBe(true,
|
||||
'Expected all unselected single-select options not to have aria-selected set.');
|
||||
|
||||
options[1].click();
|
||||
fixture.detectChanges();
|
||||
@@ -894,11 +892,44 @@ describe('MatSelect', () => {
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
expect(options[0].getAttribute('aria-selected')).toEqual('false');
|
||||
expect(options[1].getAttribute('aria-selected')).toEqual('true');
|
||||
expect(options[2].getAttribute('aria-selected')).toEqual('false');
|
||||
expect(options[1].getAttribute('aria-selected')).toEqual('true',
|
||||
'Expected selected single-select option to have aria-selected="true".');
|
||||
options.splice(1, 1);
|
||||
expect(options.every(option => !option.hasAttribute('aria-selected'))).toBe(true,
|
||||
'Expected all unselected single-select options not to have aria-selected set.');
|
||||
}));
|
||||
|
||||
it('should set aria-selected on each option for multi-select', fakeAsync(() => {
|
||||
fixture.destroy();
|
||||
|
||||
const multiFixture = TestBed.createComponent(MultiSelect);
|
||||
multiFixture.detectChanges();
|
||||
|
||||
trigger = multiFixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
|
||||
trigger.click();
|
||||
multiFixture.detectChanges();
|
||||
|
||||
options = Array.from(overlayContainerElement.querySelectorAll('mat-option'));
|
||||
|
||||
expect(options.every(option => option.hasAttribute('aria-selected') &&
|
||||
option.getAttribute('aria-selected') === 'false')).toBe(true,
|
||||
'Expected all unselected multi-select options to have aria-selected="false".');
|
||||
|
||||
options[1].click();
|
||||
multiFixture.detectChanges();
|
||||
|
||||
trigger.click();
|
||||
multiFixture.detectChanges();
|
||||
flush();
|
||||
|
||||
expect(options[1].getAttribute('aria-selected')).toEqual('true',
|
||||
'Expected selected multi-select option to have aria-selected="true".');
|
||||
options.splice(1, 1);
|
||||
expect(options.every(option => option.hasAttribute('aria-selected') &&
|
||||
option.getAttribute('aria-selected') === 'false')).toBe(true,
|
||||
'Expected all unselected multi-select options to have aria-selected="false".');
|
||||
}));
|
||||
|
||||
it('should set the tabindex of each option according to disabled state', fakeAsync(() => {
|
||||
expect(options[0].getAttribute('tabindex')).toEqual('0');
|
||||
expect(options[1].getAttribute('tabindex')).toEqual('0');
|
||||
|
||||
Vendored
+1
@@ -244,6 +244,7 @@ export declare class MatOption implements AfterViewChecked, OnDestroy {
|
||||
value: any;
|
||||
readonly viewValue: string;
|
||||
constructor(_element: ElementRef<HTMLElement>, _changeDetectorRef: ChangeDetectorRef, _parent: MatOptionParentComponent, group: MatOptgroup);
|
||||
_getAriaSelected(): boolean | null;
|
||||
_getHostElement(): HTMLElement;
|
||||
_getTabIndex(): string;
|
||||
_handleKeydown(event: KeyboardEvent): void;
|
||||
|
||||
Reference in New Issue
Block a user