Skip to content

Commit 70e249a

Browse files
committed
feat(autocomplete): add screenreader support
1 parent 41ffc09 commit 70e249a

File tree

4 files changed

+104
-2
lines changed

4 files changed

+104
-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: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,93 @@ 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+
const panel = fixture.debugElement.query(By.css('.md-autocomplete-panel')).nativeElement;
412+
413+
expect(panel.getAttribute('role'))
414+
.toEqual('listbox', 'Expected role of the panel to be listbox.');
415+
});
416+
417+
it('should set aria-autocomplete to list', () => {
418+
expect(input.getAttribute('aria-autocomplete'))
419+
.toEqual('list', 'Expected aria-autocomplete attribute to equal list.');
420+
});
421+
422+
it('should set aria-multiline to false', () => {
423+
expect(input.getAttribute('aria-multiline'))
424+
.toEqual('false', 'Expected aria-multiline attribute to equal false.');
425+
});
426+
427+
it('should set aria-activedescendant based on the active option', () => {
428+
fixture.componentInstance.trigger.openPanel();
429+
fixture.detectChanges();
430+
431+
expect(input.hasAttribute('aria-activedescendant'))
432+
.toBe(false, 'Expected aria-activedescendant to be absent if no active item.');
433+
434+
const DOWN_ARROW_EVENT = new FakeKeyboardEvent(DOWN_ARROW) as KeyboardEvent;
435+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
436+
fixture.detectChanges();
437+
438+
expect(input.getAttribute('aria-activedescendant'))
439+
.toEqual(fixture.componentInstance.options.first.id,
440+
'Expected aria-activedescendant to match the active item after 1 down arrow.');
441+
442+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
443+
fixture.detectChanges();
444+
445+
expect(input.getAttribute('aria-activedescendant'))
446+
.toEqual(fixture.componentInstance.options.toArray()[1].id,
447+
'Expected aria-activedescendant to match the active item after 2 down arrows.');
448+
});
449+
450+
it('should set aria-expanded based on whether the panel is open', async(() => {
451+
expect(input.getAttribute('aria-expanded'))
452+
.toBe('false', 'Expected aria-expanded to be false while panel is closed.');
453+
454+
fixture.componentInstance.trigger.openPanel();
455+
fixture.detectChanges();
456+
457+
expect(input.getAttribute('aria-expanded'))
458+
.toBe('true', 'Expected aria-expanded to be true while panel is open.');
459+
460+
fixture.componentInstance.trigger.closePanel();
461+
fixture.detectChanges();
462+
463+
fixture.whenStable().then(() => {
464+
expect(input.getAttribute('aria-expanded'))
465+
.toBe('false', 'Expected aria-expanded to be false when panel closes again.');
466+
});
467+
}));
468+
469+
it('should set aria-owns based on the attached autocomplete', () => {
470+
fixture.componentInstance.trigger.openPanel();
471+
fixture.detectChanges();
472+
473+
const panel = fixture.debugElement.query(By.css('.md-autocomplete-panel')).nativeElement;
474+
475+
expect(input.getAttribute('aria-owns'))
476+
.toEqual(panel.getAttribute('id'), 'Expected aria-owns to match attached autocomplete.');
477+
});
478+
479+
});
480+
394481
});
395482

396483
@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)