Skip to content

Commit e349fe4

Browse files
crisbetojelbourn
authored andcommitted
fix(select): label not being read out when using mat-label in mat-form-field (#11710)
Fixes the `mat-select` label not being read out by screen readers, if it's specified via a `mat-label` on a `mat-form-field`.
1 parent 69629ad commit e349fe4

File tree

4 files changed

+62
-4
lines changed

4 files changed

+62
-4
lines changed

src/lib/form-field/form-field.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<!-- We add aria-owns as a workaround for an issue in JAWS & NVDA where the label isn't
1313
read if it comes before the control in the DOM. -->
1414
<label class="mat-form-field-label"
15+
[id]="_labelId"
1516
[attr.for]="_control.id"
1617
[attr.aria-owns]="_control.id"
1718
[class.mat-empty]="_control.empty && !_shouldAlwaysFloat"

src/lib/form-field/form-field.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,9 @@ export class MatFormField extends _MatFormFieldMixinBase
186186
// Unique id for the hint label.
187187
_hintLabelId: string = `mat-hint-${nextUniqueId++}`;
188188

189+
// Unique id for the internal form field label.
190+
_labelId = `mat-form-field-label-${nextUniqueId++}`;
191+
189192
/**
190193
* Whether the label should always float, never float or float as the user types.
191194
*

src/lib/select/select.spec.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ describe('MatSelect', () => {
124124
MultiSelect,
125125
SelectWithGroups,
126126
SelectWithGroupsAndNgContainer,
127+
SelectWithFormFieldLabel,
127128
]);
128129
}));
129130

@@ -230,6 +231,29 @@ describe('MatSelect', () => {
230231
expect(select.getAttribute('tabindex')).toEqual('0');
231232
}));
232233

234+
it('should set `aria-labelledby` to form field label if there is no placeholder', () => {
235+
fixture.destroy();
236+
237+
const labelFixture = TestBed.createComponent(SelectWithFormFieldLabel);
238+
labelFixture.detectChanges();
239+
select = labelFixture.debugElement.query(By.css('mat-select')).nativeElement;
240+
241+
expect(select.getAttribute('aria-labelledby')).toBeTruthy();
242+
expect(select.getAttribute('aria-labelledby'))
243+
.toBe(labelFixture.nativeElement.querySelector('label').getAttribute('id'));
244+
});
245+
246+
it('should not set `aria-labelledby` if there is a placeholder', () => {
247+
fixture.destroy();
248+
249+
const labelFixture = TestBed.createComponent(SelectWithFormFieldLabel);
250+
labelFixture.componentInstance.placeholder = 'Thing selector';
251+
labelFixture.detectChanges();
252+
select = labelFixture.debugElement.query(By.css('mat-select')).nativeElement;
253+
254+
expect(select.getAttribute('aria-labelledby')).toBeFalsy();
255+
});
256+
233257
it('should select options via the UP/DOWN arrow keys on a closed select', fakeAsync(() => {
234258
const formControl = fixture.componentInstance.control;
235259
const options = fixture.componentInstance.options.toArray();
@@ -4561,3 +4585,18 @@ class SelectWithoutOptionCentering {
45614585
@ViewChild(MatSelect) select: MatSelect;
45624586
@ViewChildren(MatOption) options: QueryList<MatOption>;
45634587
}
4588+
4589+
@Component({
4590+
template: `
4591+
<mat-form-field>
4592+
<mat-label>Select a thing</mat-label>
4593+
4594+
<mat-select [placeholder]="placeholder">
4595+
<mat-option value="thing">A thing</mat-option>
4596+
</mat-select>
4597+
</mat-form-field>
4598+
`
4599+
})
4600+
class SelectWithFormFieldLabel {
4601+
placeholder: string;
4602+
}

src/lib/select/select.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ export class MatSelectTrigger {}
189189
'role': 'listbox',
190190
'[attr.id]': 'id',
191191
'[attr.tabindex]': 'tabIndex',
192-
'[attr.aria-label]': '_ariaLabel',
193-
'[attr.aria-labelledby]': 'ariaLabelledby',
192+
'[attr.aria-label]': '_getAriaLabel()',
193+
'[attr.aria-labelledby]': '_getAriaLabelledby()',
194194
'[attr.aria-required]': 'required.toString()',
195195
'[attr.aria-disabled]': 'disabled.toString()',
196196
'[attr.aria-invalid]': 'errorState',
@@ -1017,12 +1017,27 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
10171017
}
10181018

10191019
/** Returns the aria-label of the select component. */
1020-
get _ariaLabel(): string | null {
1021-
// If an ariaLabelledby value has been set, the select should not overwrite the
1020+
_getAriaLabel(): string | null {
1021+
// If an ariaLabelledby value has been set by the consumer, the select should not overwrite the
10221022
// `aria-labelledby` value by setting the ariaLabel to the placeholder.
10231023
return this.ariaLabelledby ? null : this.ariaLabel || this.placeholder;
10241024
}
10251025

1026+
/** Returns the aria-labelledby of the select component. */
1027+
_getAriaLabelledby(): string | null {
1028+
if (this.ariaLabelledby) {
1029+
return this.ariaLabelledby;
1030+
}
1031+
1032+
// Note: we use `_getAriaLabel` here, because we want to check whether there's a
1033+
// computed label. `this.ariaLabel` is only the user-specified label.
1034+
if (!this._parentFormField || this._getAriaLabel()) {
1035+
return null;
1036+
}
1037+
1038+
return this._parentFormField._labelId || null;
1039+
}
1040+
10261041
/** Determines the `aria-activedescendant` to be set on the host. */
10271042
_getAriaActiveDescendant(): string | null {
10281043
if (this.panelOpen && this._keyManager && this._keyManager.activeItem) {

0 commit comments

Comments
 (0)