mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(autocomplete): add screenreader support (#2729)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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({
|
||||
|
||||
@@ -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',
|
||||
@@ -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++}`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user