Skip to content

Commit

Permalink
feat(autocomplete): add screenreader support (#2729)
Browse files Browse the repository at this point in the history
  • Loading branch information
kara authored Jan 23, 2017
1 parent 41ffc09 commit 4ad8b82
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 2 deletions.
8 changes: 7 additions & 1 deletion src/lib/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,15 @@ export const MD_AUTOCOMPLETE_PANEL_OFFSET = 6;
@Directive({
selector: 'input[mdAutocomplete], input[matAutocomplete]',
host: {
'role': 'combobox',
'autocomplete': 'off',
'aria-autocomplete': 'list',
'aria-multiline': 'false',
'[attr.aria-activedescendant]': 'activeOption?.id',
'[attr.aria-expanded]': 'panelOpen.toString()',
'[attr.aria-owns]': 'autocomplete?.id',
'(focus)': 'openPanel()',
'(keydown)': '_handleKeydown($event)',
'autocomplete': 'off'
}
})
export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/autocomplete/autocomplete.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div class="md-autocomplete-panel">
<div class="md-autocomplete-panel" role="listbox" [id]="id">
<ng-content></ng-content>
</div>
</template>
90 changes: 90 additions & 0 deletions src/lib/autocomplete/autocomplete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,96 @@ describe('MdAutocomplete', () => {

});

describe('aria', () => {
let fixture: ComponentFixture<SimpleAutocomplete>;
let input: HTMLInputElement;

beforeEach(() => {
fixture = TestBed.createComponent(SimpleAutocomplete);
fixture.detectChanges();

input = fixture.debugElement.query(By.css('input')).nativeElement;
});

it('should set role of input to combobox', () => {
expect(input.getAttribute('role'))
.toEqual('combobox', 'Expected role of input to be combobox.');
});

it('should set role of autocomplete panel to listbox', () => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();

const panel = fixture.debugElement.query(By.css('.md-autocomplete-panel')).nativeElement;

expect(panel.getAttribute('role'))
.toEqual('listbox', 'Expected role of the panel to be listbox.');
});

it('should set aria-autocomplete to list', () => {
expect(input.getAttribute('aria-autocomplete'))
.toEqual('list', 'Expected aria-autocomplete attribute to equal list.');
});

it('should set aria-multiline to false', () => {
expect(input.getAttribute('aria-multiline'))
.toEqual('false', 'Expected aria-multiline attribute to equal false.');
});

it('should set aria-activedescendant based on the active option', () => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();

expect(input.hasAttribute('aria-activedescendant'))
.toBe(false, 'Expected aria-activedescendant to be absent if no active item.');

const DOWN_ARROW_EVENT = new FakeKeyboardEvent(DOWN_ARROW) as KeyboardEvent;
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
fixture.detectChanges();

expect(input.getAttribute('aria-activedescendant'))
.toEqual(fixture.componentInstance.options.first.id,
'Expected aria-activedescendant to match the active item after 1 down arrow.');

fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
fixture.detectChanges();

expect(input.getAttribute('aria-activedescendant'))
.toEqual(fixture.componentInstance.options.toArray()[1].id,
'Expected aria-activedescendant to match the active item after 2 down arrows.');
});

it('should set aria-expanded based on whether the panel is open', async(() => {
expect(input.getAttribute('aria-expanded'))
.toBe('false', 'Expected aria-expanded to be false while panel is closed.');

fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();

expect(input.getAttribute('aria-expanded'))
.toBe('true', 'Expected aria-expanded to be true while panel is open.');

fixture.componentInstance.trigger.closePanel();
fixture.detectChanges();

fixture.whenStable().then(() => {
expect(input.getAttribute('aria-expanded'))
.toBe('false', 'Expected aria-expanded to be false when panel closes again.');
});
}));

it('should set aria-owns based on the attached autocomplete', () => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();

const panel = fixture.debugElement.query(By.css('.md-autocomplete-panel')).nativeElement;

expect(input.getAttribute('aria-owns'))
.toEqual(panel.getAttribute('id'), 'Expected aria-owns to match attached autocomplete.');
});

});

});

@Component({
Expand Down
9 changes: 9 additions & 0 deletions src/lib/autocomplete/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import {
} from '@angular/core';
import {MdOption} from '../core';

/**
* Autocomplete IDs need to be unique across components, so this counter exists outside of
* the component definition.
*/
let _uniqueAutocompleteIdCounter = 0;

@Component({
moduleId: module.id,
selector: 'md-autocomplete, mat-autocomplete',
Expand All @@ -20,5 +26,8 @@ export class MdAutocomplete {

@ViewChild(TemplateRef) template: TemplateRef<any>;
@ContentChildren(MdOption) options: QueryList<MdOption>;

/** Unique ID to be used by autocomplete trigger's "aria-owns" property. */
id: string = `md-autocomplete-${_uniqueAutocompleteIdCounter++}`;
}

0 comments on commit 4ad8b82

Please sign in to comment.