Skip to content

Commit 83055fb

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

File tree

4 files changed

+95
-2
lines changed

4 files changed

+95
-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: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,82 @@ describe('MdAutocomplete', () => {
234234
.toBe(false, `Expected control to stay pristine if value is set programmatically.`);
235235
});
236236

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

239315
});

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)