Skip to content

Commit e9759ab

Browse files
committed
feat(autocomplete): add screenreader support
1 parent 5def001 commit e9759ab

File tree

4 files changed

+106
-2
lines changed

4 files changed

+106
-2
lines changed

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,15 @@ export const MD_AUTOCOMPLETE_PANEL_OFFSET = 6;
1919
@Directive({
2020
selector: 'input[mdAutocomplete], input[matAutocomplete]',
2121
host: {
22-
'(focus)': 'openPanel()'
22+
'role': 'combobox',
23+
'autocomplete': 'off',
24+
'aria-autocomplete': 'list',
25+
'aria-multiline': 'false',
26+
'[attr.aria-activedescendant]': 'activeOption?.id',
27+
'[attr.aria-expanded]': 'panelOpen.toString()',
28+
'[attr.aria-owns]': 'autocomplete?.id',
29+
'(focus)': 'openPanel()',
30+
'(keydown)': '_handleKeydown($event)',
2331
}
2432
})
2533
export class MdAutocompleteTrigger implements 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
@@ -236,6 +236,93 @@ describe('MdAutocomplete', () => {
236236

237237
});
238238

239+
describe('aria', () => {
240+
let fixture: ComponentFixture<SimpleAutocomplete>;
241+
let input: HTMLInputElement;
242+
243+
beforeEach(() => {
244+
fixture = TestBed.createComponent(SimpleAutocomplete);
245+
fixture.detectChanges();
246+
247+
input = fixture.debugElement.query(By.css('input')).nativeElement;
248+
});
249+
250+
it('should set role of input to combobox', () => {
251+
expect(input.getAttribute('role'))
252+
.toEqual('combobox', 'Expected role of input to be combobox.');
253+
});
254+
255+
it('should set role of autocomplete panel to listbox', () => {
256+
const panel = fixture.debugElement.query(By.css('.md-autocomplete-panel')).nativeElement;
257+
258+
expect(panel.getAttribute('role'))
259+
.toEqual('listbox', 'Expected role of the panel to be listbox.');
260+
});
261+
262+
it('should set aria-autocomplete to list', () => {
263+
expect(input.getAttribute('aria-autocomplete'))
264+
.toEqual('list', 'Expected aria-autocomplete attribute to equal list.');
265+
});
266+
267+
it('should set aria-multiline to false', () => {
268+
expect(input.getAttribute('aria-multiline'))
269+
.toEqual('false', 'Expected aria-multiline attribute to equal false.');
270+
});
271+
272+
it('should set aria-activedescendant based on the active option', () => {
273+
fixture.componentInstance.trigger.openPanel();
274+
fixture.detectChanges();
275+
276+
expect(input.hasAttribute('aria-activedescendant'))
277+
.toBe(false, 'Expected aria-activedescendant to be absent if no active item.');
278+
279+
const DOWN_ARROW_EVENT = new FakeKeyboardEvent(DOWN_ARROW) as KeyboardEvent;
280+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
281+
fixture.detectChanges();
282+
283+
expect(input.getAttribute('aria-activedescendant'))
284+
.toEqual(fixture.componentInstance.options.first.id,
285+
'Expected aria-activedescendant to match the active item after 1 down arrow.');
286+
287+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
288+
fixture.detectChanges();
289+
290+
expect(input.getAttribute('aria-activedescendant'))
291+
.toEqual(fixture.componentInstance.options.toArray()[1].id,
292+
'Expected aria-activedescendant to match the active item after 2 down arrows.');
293+
});
294+
295+
it('should set aria-expanded based on whether the panel is open', async(() => {
296+
expect(input.getAttribute('aria-expanded'))
297+
.toBe('false', 'Expected aria-expanded to be false while panel is closed.');
298+
299+
fixture.componentInstance.trigger.openPanel();
300+
fixture.detectChanges();
301+
302+
expect(input.getAttribute('aria-expanded'))
303+
.toBe('true', 'Expected aria-expanded to be true while panel is open.');
304+
305+
fixture.componentInstance.trigger.closePanel();
306+
fixture.detectChanges();
307+
308+
fixture.whenStable().then(() => {
309+
expect(input.getAttribute('aria-expanded'))
310+
.toBe('false', 'Expected aria-expanded to be false when panel closes again.');
311+
});
312+
}));
313+
314+
it('should set aria-owns based on the attached autocomplete', () => {
315+
fixture.componentInstance.trigger.openPanel();
316+
fixture.detectChanges();
317+
318+
const panel = fixture.debugElement.query(By.css('.md-autocomplete-panel')).nativeElement;
319+
320+
expect(input.getAttribute('aria-owns'))
321+
.toEqual(panel.getAttribute('id'), 'Expected aria-owns to match attached autocomplete.');
322+
});
323+
324+
});
325+
239326
});
240327

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