diff --git a/src/lib/form-field/form-field.html b/src/lib/form-field/form-field.html index 13254c0732e..4323d75841c 100644 --- a/src/lib/form-field/form-field.html +++ b/src/lib/form-field/form-field.html @@ -12,6 +12,7 @@ { MultiSelect, SelectWithGroups, SelectWithGroupsAndNgContainer, + SelectWithFormFieldLabel, ]); })); @@ -230,6 +231,29 @@ describe('MatSelect', () => { expect(select.getAttribute('tabindex')).toEqual('0'); })); + it('should set `aria-labelledby` to form field label if there is no placeholder', () => { + fixture.destroy(); + + const labelFixture = TestBed.createComponent(SelectWithFormFieldLabel); + labelFixture.detectChanges(); + select = labelFixture.debugElement.query(By.css('mat-select')).nativeElement; + + expect(select.getAttribute('aria-labelledby')).toBeTruthy(); + expect(select.getAttribute('aria-labelledby')) + .toBe(labelFixture.nativeElement.querySelector('label').getAttribute('id')); + }); + + it('should not set `aria-labelledby` if there is a placeholder', () => { + fixture.destroy(); + + const labelFixture = TestBed.createComponent(SelectWithFormFieldLabel); + labelFixture.componentInstance.placeholder = 'Thing selector'; + labelFixture.detectChanges(); + select = labelFixture.debugElement.query(By.css('mat-select')).nativeElement; + + expect(select.getAttribute('aria-labelledby')).toBeFalsy(); + }); + it('should select options via the UP/DOWN arrow keys on a closed select', fakeAsync(() => { const formControl = fixture.componentInstance.control; const options = fixture.componentInstance.options.toArray(); @@ -4561,3 +4585,18 @@ class SelectWithoutOptionCentering { @ViewChild(MatSelect) select: MatSelect; @ViewChildren(MatOption) options: QueryList; } + +@Component({ + template: ` + + Select a thing + + + A thing + + + ` +}) +class SelectWithFormFieldLabel { + placeholder: string; +} diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index 92db23b2a12..71133aa039a 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -189,8 +189,8 @@ export class MatSelectTrigger {} 'role': 'listbox', '[attr.id]': 'id', '[attr.tabindex]': 'tabIndex', - '[attr.aria-label]': '_ariaLabel', - '[attr.aria-labelledby]': 'ariaLabelledby', + '[attr.aria-label]': '_getAriaLabel()', + '[attr.aria-labelledby]': '_getAriaLabelledby()', '[attr.aria-required]': 'required.toString()', '[attr.aria-disabled]': 'disabled.toString()', '[attr.aria-invalid]': 'errorState', @@ -1017,12 +1017,27 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit, } /** Returns the aria-label of the select component. */ - get _ariaLabel(): string | null { - // If an ariaLabelledby value has been set, the select should not overwrite the + _getAriaLabel(): string | null { + // If an ariaLabelledby value has been set by the consumer, the select should not overwrite the // `aria-labelledby` value by setting the ariaLabel to the placeholder. return this.ariaLabelledby ? null : this.ariaLabel || this.placeholder; } + /** Returns the aria-labelledby of the select component. */ + _getAriaLabelledby(): string | null { + if (this.ariaLabelledby) { + return this.ariaLabelledby; + } + + // Note: we use `_getAriaLabel` here, because we want to check whether there's a + // computed label. `this.ariaLabel` is only the user-specified label. + if (!this._parentFormField || this._getAriaLabel()) { + return null; + } + + return this._parentFormField._labelId || null; + } + /** Determines the `aria-activedescendant` to be set on the host. */ _getAriaActiveDescendant(): string | null { if (this.panelOpen && this._keyManager && this._keyManager.activeItem) {