mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(select): add aria-owns property (#1898)
This commit is contained in:
@@ -10,6 +10,12 @@ import {
|
||||
import {ENTER, SPACE} from '../core/keyboard/keycodes';
|
||||
import {coerceBooleanProperty} from '../core/coersion/boolean-property';
|
||||
|
||||
/**
|
||||
* Option IDs need to be unique across components, so this counter exists outside of
|
||||
* the component definition.
|
||||
*/
|
||||
let _uniqueIdCounter = 0;
|
||||
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'md-option',
|
||||
@@ -17,6 +23,7 @@ import {coerceBooleanProperty} from '../core/coersion/boolean-property';
|
||||
'role': 'option',
|
||||
'[attr.tabindex]': '_getTabIndex()',
|
||||
'[class.md-selected]': 'selected',
|
||||
'[id]': 'id',
|
||||
'[attr.aria-selected]': 'selected.toString()',
|
||||
'[attr.aria-disabled]': 'disabled.toString()',
|
||||
'[class.md-option-disabled]': 'disabled',
|
||||
@@ -33,6 +40,11 @@ export class MdOption {
|
||||
/** Whether the option is disabled. */
|
||||
private _disabled: boolean = false;
|
||||
|
||||
private _id: string = `md-select-option-${_uniqueIdCounter++}`;
|
||||
|
||||
/** The unique ID of the option. */
|
||||
get id() { return this._id; }
|
||||
|
||||
/** The form value of the option. */
|
||||
@Input() value: any;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {TestBed, async, ComponentFixture} from '@angular/core/testing';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {Component, QueryList, ViewChild, ViewChildren} from '@angular/core';
|
||||
import {Component, DebugElement, QueryList, ViewChild, ViewChildren} from '@angular/core';
|
||||
import {MdSelectModule} from './index';
|
||||
import {OverlayContainer} from '../core/overlay/overlay-container';
|
||||
import {MdSelect} from './select';
|
||||
@@ -16,7 +16,7 @@ describe('MdSelect', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MdSelectModule.forRoot(), ReactiveFormsModule, FormsModule],
|
||||
declarations: [BasicSelect, NgModelSelect],
|
||||
declarations: [BasicSelect, NgModelSelect, ManySelects],
|
||||
providers: [
|
||||
{provide: OverlayContainer, useFactory: () => {
|
||||
overlayContainerElement = document.createElement('div');
|
||||
@@ -547,17 +547,14 @@ describe('MdSelect', () => {
|
||||
});
|
||||
|
||||
describe('accessibility', () => {
|
||||
let fixture: ComponentFixture<BasicSelect>;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BasicSelect);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('for select', () => {
|
||||
let fixture: ComponentFixture<BasicSelect>;
|
||||
let select: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BasicSelect);
|
||||
fixture.detectChanges();
|
||||
select = fixture.debugElement.query(By.css('md-select')).nativeElement;
|
||||
});
|
||||
|
||||
@@ -614,14 +611,16 @@ describe('MdSelect', () => {
|
||||
expect(select.getAttribute('tabindex')).toEqual('0');
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
describe('for options', () => {
|
||||
let fixture: ComponentFixture<BasicSelect>;
|
||||
let trigger: HTMLElement;
|
||||
let options: NodeListOf<HTMLElement>;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BasicSelect);
|
||||
fixture.detectChanges();
|
||||
trigger = fixture.debugElement.query(By.css('.md-select-trigger')).nativeElement;
|
||||
trigger.click();
|
||||
fixture.detectChanges();
|
||||
@@ -673,6 +672,78 @@ describe('MdSelect', () => {
|
||||
|
||||
});
|
||||
|
||||
describe('aria-owns', () => {
|
||||
let fixture: ComponentFixture<ManySelects>;
|
||||
let triggers: DebugElement[];
|
||||
let options: NodeListOf<HTMLElement>;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ManySelects);
|
||||
fixture.detectChanges();
|
||||
triggers = fixture.debugElement.queryAll(By.css('.md-select-trigger'));
|
||||
|
||||
triggers[0].nativeElement.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
options =
|
||||
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
|
||||
});
|
||||
|
||||
it('should set aria-owns properly', async(() => {
|
||||
const selects = fixture.debugElement.queryAll(By.css('md-select'));
|
||||
|
||||
expect(selects[0].nativeElement.getAttribute('aria-owns'))
|
||||
.toContain(options[0].id, `Expected aria-owns to contain IDs of its child options.`);
|
||||
expect(selects[0].nativeElement.getAttribute('aria-owns'))
|
||||
.toContain(options[1].id, `Expected aria-owns to contain IDs of its child options.`);
|
||||
|
||||
const backdrop =
|
||||
overlayContainerElement.querySelector('.md-overlay-backdrop') as HTMLElement;
|
||||
backdrop.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
triggers[1].nativeElement.click();
|
||||
|
||||
fixture.detectChanges();
|
||||
options =
|
||||
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
|
||||
expect(selects[1].nativeElement.getAttribute('aria-owns'))
|
||||
.toContain(options[0].id, `Expected aria-owns to contain IDs of its child options.`);
|
||||
expect(selects[1].nativeElement.getAttribute('aria-owns'))
|
||||
.toContain(options[1].id, `Expected aria-owns to contain IDs of its child options.`);
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
it('should set the option id properly', async(() => {
|
||||
let firstOptionID = options[0].id;
|
||||
|
||||
expect(options[0].id)
|
||||
.toContain('md-select-option', `Expected option ID to have the correct prefix.`);
|
||||
expect(options[0].id).not.toEqual(options[1].id, `Expected option IDs to be unique.`);
|
||||
|
||||
const backdrop =
|
||||
overlayContainerElement.querySelector('.md-overlay-backdrop') as HTMLElement;
|
||||
backdrop.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
triggers[1].nativeElement.click();
|
||||
|
||||
fixture.detectChanges();
|
||||
options =
|
||||
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
|
||||
expect(options[0].id)
|
||||
.toContain('md-select-option', `Expected option ID to have the correct prefix.`);
|
||||
expect(options[0].id).not.toEqual(firstOptionID, `Expected option IDs to be unique.`);
|
||||
expect(options[0].id).not.toEqual(options[1].id, `Expected option IDs to be unique.`);
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -720,6 +791,21 @@ class NgModelSelect {
|
||||
@ViewChildren(MdOption) options: QueryList<MdOption>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'many-selects',
|
||||
template: `
|
||||
<md-select placeholder="First">
|
||||
<md-option value="one">one</md-option>
|
||||
<md-option value="two">two</md-option>
|
||||
</md-select>
|
||||
<md-select placeholder="Second">
|
||||
<md-option value="three">three</md-option>
|
||||
<md-option value="four">four</md-option>
|
||||
</md-select>
|
||||
`
|
||||
})
|
||||
class ManySelects {}
|
||||
|
||||
|
||||
/**
|
||||
* TODO: Move this to core testing utility until Angular has event faking
|
||||
|
||||
+24
-10
@@ -35,8 +35,9 @@ import {ConnectedOverlayPositionChange} from '../core/overlay/position/connected
|
||||
'[attr.aria-label]': 'placeholder',
|
||||
'[attr.aria-required]': 'required.toString()',
|
||||
'[attr.aria-disabled]': 'disabled.toString()',
|
||||
'[class.md-select-disabled]': 'disabled',
|
||||
'[attr.aria-invalid]': '_control?.invalid || "false"',
|
||||
'[attr.aria-owns]': '_optionIds',
|
||||
'[class.md-select-disabled]': 'disabled',
|
||||
'(keydown)': '_handleKeydown($event)',
|
||||
'(blur)': '_onBlur()'
|
||||
},
|
||||
@@ -76,7 +77,10 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
|
||||
_onChange: (value: any) => void;
|
||||
|
||||
/** View -> model callback called when select has been touched */
|
||||
_onTouched: Function;
|
||||
_onTouched = () => {};
|
||||
|
||||
/** The IDs of child options to be passed to the aria-owns attribute. */
|
||||
_optionIds: string = '';
|
||||
|
||||
/** The value of the select panel's transform-origin property. */
|
||||
_transformOrigin: string = 'top';
|
||||
@@ -130,17 +134,15 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
|
||||
|
||||
constructor(private _element: ElementRef, private _renderer: Renderer,
|
||||
@Optional() private _dir: Dir, @Optional() public _control: NgControl) {
|
||||
this._control.valueAccessor = this;
|
||||
if (this._control) {
|
||||
this._control.valueAccessor = this;
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this._initKeyManager();
|
||||
this._listenToOptions();
|
||||
|
||||
this._changeSubscription = this.options.changes.subscribe(() => {
|
||||
this._dropSubscriptions();
|
||||
this._listenToOptions();
|
||||
});
|
||||
this._resetOptions();
|
||||
this._changeSubscription = this.options.changes.subscribe(() => this._resetOptions());
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -196,7 +198,7 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
|
||||
* by the user. Part of the ControlValueAccessor interface required
|
||||
* to integrate with Angular's core forms API.
|
||||
*/
|
||||
registerOnTouched(fn: Function): void {
|
||||
registerOnTouched(fn: () => {}): void {
|
||||
this._onTouched = fn;
|
||||
}
|
||||
|
||||
@@ -294,6 +296,13 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
|
||||
});
|
||||
}
|
||||
|
||||
/** Drops current option subscriptions and IDs and resets from scratch. */
|
||||
private _resetOptions(): void {
|
||||
this._dropSubscriptions();
|
||||
this._listenToOptions();
|
||||
this._setOptionIds();
|
||||
}
|
||||
|
||||
/** Listens to selection events on each option. */
|
||||
private _listenToOptions(): void {
|
||||
this.options.forEach((option: MdOption) => {
|
||||
@@ -313,6 +322,11 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
|
||||
this._subscriptions = [];
|
||||
}
|
||||
|
||||
/** Records option IDs to pass to the aria-owns property. */
|
||||
private _setOptionIds() {
|
||||
this._optionIds = this.options.map(option => option.id).join(' ');
|
||||
}
|
||||
|
||||
/** When a new option is selected, deselects the others and closes the panel. */
|
||||
private _onSelect(option: MdOption): void {
|
||||
this._selected = option;
|
||||
|
||||
Reference in New Issue
Block a user