Skip to content

Commit bd7f240

Browse files
committed
feat(autocomplete): add screenreader support (#2729)
1 parent fcea9d4 commit bd7f240

File tree

4 files changed

+107
-2
lines changed

4 files changed

+107
-2
lines changed

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,15 @@ export const MD_AUTOCOMPLETE_PANEL_OFFSET = 6;
2121
@Directive({
2222
selector: 'input[mdAutocomplete], input[matAutocomplete]',
2323
host: {
24+
'role': 'combobox',
25+
'autocomplete': 'off',
26+
'aria-autocomplete': 'list',
27+
'aria-multiline': 'false',
28+
'[attr.aria-activedescendant]': 'activeOption?.id',
29+
'[attr.aria-expanded]': 'panelOpen.toString()',
30+
'[attr.aria-owns]': 'autocomplete?.id',
2431
'(focus)': 'openPanel()',
2532
'(keydown)': '_handleKeydown($event)',
26-
'autocomplete': 'off'
2733
}
2834
})
2935
export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<div class="md-autocomplete-panel">
2+
<div class="md-autocomplete-panel" role="listbox" [id]="id">
33
<ng-content></ng-content>
44
</div>
55
</template>

src/lib/autocomplete/autocomplete.spec.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,96 @@ describe('MdAutocomplete', () => {
391391

392392
});
393393

394+
describe('aria', () => {
395+
let fixture: ComponentFixture<SimpleAutocomplete>;
396+
let input: HTMLInputElement;
397+
398+
beforeEach(() => {
399+
fixture = TestBed.createComponent(SimpleAutocomplete);
400+
fixture.detectChanges();
401+
402+
input = fixture.debugElement.query(By.css('input')).nativeElement;
403+
});
404+
405+
it('should set role of input to combobox', () => {
406+
expect(input.getAttribute('role'))
407+
.toEqual('combobox', 'Expected role of input to be combobox.');
408+
});
409+
410+
it('should set role of autocomplete panel to listbox', () => {
411+
fixture.componentInstance.trigger.openPanel();
412+
fixture.detectChanges();
413+
414+
const panel = fixture.debugElement.query(By.css('.md-autocomplete-panel')).nativeElement;
415+
416+
expect(panel.getAttribute('role'))
417+
.toEqual('listbox', 'Expected role of the panel to be listbox.');
418+
});
419+
420+
it('should set aria-autocomplete to list', () => {
421+
expect(input.getAttribute('aria-autocomplete'))
422+
.toEqual('list', 'Expected aria-autocomplete attribute to equal list.');
423+
});
424+
425+
it('should set aria-multiline to false', () => {
426+
expect(input.getAttribute('aria-multiline'))
427+
.toEqual('false', 'Expected aria-multiline attribute to equal false.');
428+
});
429+
430+
it('should set aria-activedescendant based on the active option', () => {
431+
fixture.componentInstance.trigger.openPanel();
432+
fixture.detectChanges();
433+
434+
expect(input.hasAttribute('aria-activedescendant'))
435+
.toBe(false, 'Expected aria-activedescendant to be absent if no active item.');
436+
437+
const DOWN_ARROW_EVENT = new FakeKeyboardEvent(DOWN_ARROW) as KeyboardEvent;
438+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
439+
fixture.detectChanges();
440+
441+
expect(input.getAttribute('aria-activedescendant'))
442+
.toEqual(fixture.componentInstance.options.first.id,
443+
'Expected aria-activedescendant to match the active item after 1 down arrow.');
444+
445+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
446+
fixture.detectChanges();
447+
448+
expect(input.getAttribute('aria-activedescendant'))
449+
.toEqual(fixture.componentInstance.options.toArray()[1].id,
450+
'Expected aria-activedescendant to match the active item after 2 down arrows.');
451+
});
452+
453+
it('should set aria-expanded based on whether the panel is open', async(() => {
454+
expect(input.getAttribute('aria-expanded'))
455+
.toBe('false', 'Expected aria-expanded to be false while panel is closed.');
456+
457+
fixture.componentInstance.trigger.openPanel();
458+
fixture.detectChanges();
459+
460+
expect(input.getAttribute('aria-expanded'))
461+
.toBe('true', 'Expected aria-expanded to be true while panel is open.');
462+
463+
fixture.componentInstance.trigger.closePanel();
464+
fixture.detectChanges();
465+
466+
fixture.whenStable().then(() => {
467+
expect(input.getAttribute('aria-expanded'))
468+
.toBe('false', 'Expected aria-expanded to be false when panel closes again.');
469+
});
470+
}));
471+
472+
it('should set aria-owns based on the attached autocomplete', () => {
473+
fixture.componentInstance.trigger.openPanel();
474+
fixture.detectChanges();
475+
476+
const panel = fixture.debugElement.query(By.css('.md-autocomplete-panel')).nativeElement;
477+
478+
expect(input.getAttribute('aria-owns'))
479+
.toEqual(panel.getAttribute('id'), 'Expected aria-owns to match attached autocomplete.');
480+
});
481+
482+
});
483+
394484
});
395485

396486
@Component({

src/lib/autocomplete/autocomplete.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ import {
88
} from '@angular/core';
99
import {MdOption} from '../core';
1010

11+
/**
12+
* Autocomplete IDs need to be unique across components, so this counter exists outside of
13+
* the component definition.
14+
*/
15+
let _uniqueAutocompleteIdCounter = 0;
16+
1117
@Component({
1218
moduleId: module.id,
1319
selector: 'md-autocomplete, mat-autocomplete',
@@ -20,5 +26,8 @@ export class MdAutocomplete {
2026

2127
@ViewChild(TemplateRef) template: TemplateRef<any>;
2228
@ContentChildren(MdOption) options: QueryList<MdOption>;
29+
30+
/** Unique ID to be used by autocomplete trigger's "aria-owns" property. */
31+
id: string = `md-autocomplete-${_uniqueAutocompleteIdCounter++}`;
2332
}
2433

0 commit comments

Comments
 (0)