Skip to content

Commit 9b67d67

Browse files
authored
fix(material/select): trim aria-labelledby (angular#22251)
If the form field doesn't have a label, we can end up with an `aria-labelledby` which has a leading space. This appears to be flagged as invalid by some a11y tools. Fixes angular#22192.
1 parent 50b3207 commit 9b67d67

File tree

3 files changed

+53
-9
lines changed

3 files changed

+53
-9
lines changed

src/material-experimental/mdc-select/select.spec.ts

+25-1
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,17 @@ describe('MDC-based MatSelect', () => {
212212
expect(select.getAttribute('aria-labelledby')).toBe(`${labelId} ${valueId}`);
213213
}));
214214

215+
it('should trim the trigger aria-labelledby when there is no label', fakeAsync(() => {
216+
fixture.componentInstance.hasLabel = false;
217+
fixture.detectChanges();
218+
flush();
219+
fixture.detectChanges();
220+
221+
// Note that we assert that there are no spaces around the value.
222+
const valueId = fixture.nativeElement.querySelector('.mat-mdc-select-value').id;
223+
expect(select.getAttribute('aria-labelledby')).toBe(`${valueId}`);
224+
}));
225+
215226
it('should set the tabindex of the select to 0 by default', fakeAsync(() => {
216227
expect(select.getAttribute('tabindex')).toEqual('0');
217228
}));
@@ -976,6 +987,18 @@ describe('MDC-based MatSelect', () => {
976987
expect(panel.getAttribute('aria-labelledby')).toBe(`${labelId} myLabelId`);
977988
}));
978989

990+
it('should trim the custom panel aria-labelledby when there is no label', fakeAsync(() => {
991+
fixture.componentInstance.hasLabel = false;
992+
fixture.componentInstance.ariaLabelledby = 'myLabelId';
993+
fixture.componentInstance.select.open();
994+
fixture.detectChanges();
995+
flush();
996+
997+
// Note that we assert that there are no spaces around the value.
998+
const panel = document.querySelector('.mat-mdc-select-panel')!;
999+
expect(panel.getAttribute('aria-labelledby')).toBe(`myLabelId`);
1000+
}));
1001+
9791002
it('should clear aria-labelledby from the panel if an aria-label is set', fakeAsync(() => {
9801003
fixture.componentInstance.ariaLabel = 'My label';
9811004
fixture.componentInstance.select.open();
@@ -3835,7 +3858,7 @@ describe('MDC-based MatSelect', () => {
38353858
template: `
38363859
<div [style.height.px]="heightAbove"></div>
38373860
<mat-form-field>
3838-
<mat-label>Select a food</mat-label>
3861+
<mat-label *ngIf="hasLabel">Select a food</mat-label>
38393862
<mat-select placeholder="Food" [formControl]="control" [required]="isRequired"
38403863
[tabIndex]="tabIndexOverride" [aria-label]="ariaLabel" [aria-labelledby]="ariaLabelledby"
38413864
[panelClass]="panelClass" [disableRipple]="disableRipple"
@@ -3863,6 +3886,7 @@ class BasicSelect {
38633886
isRequired: boolean;
38643887
heightAbove = 0;
38653888
heightBelow = 0;
3889+
hasLabel = true;
38663890
tabIndexOverride: number;
38673891
ariaLabel: string;
38683892
ariaLabelledby: string;

src/material/select/select.spec.ts

+23
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,16 @@ describe('MatSelect', () => {
215215
expect(select.getAttribute('aria-labelledby')).toBe(`${labelId} ${valueId}`);
216216
}));
217217

218+
it('should trim the trigger aria-labelledby when there is no label', fakeAsync(() => {
219+
// Reset the `placeholder` which also controls the label of the form field.
220+
fixture.componentInstance.select.placeholder = '';
221+
fixture.detectChanges();
222+
223+
// Note that we assert that there are no spaces around the value.
224+
const valueId = fixture.nativeElement.querySelector('.mat-select-value').id;
225+
expect(select.getAttribute('aria-labelledby')).toBe(`${valueId}`);
226+
}));
227+
218228
it('should set the tabindex of the select to 0 by default', fakeAsync(() => {
219229
expect(select.getAttribute('tabindex')).toEqual('0');
220230
}));
@@ -979,6 +989,19 @@ describe('MatSelect', () => {
979989
expect(panel.getAttribute('aria-labelledby')).toBe(`${labelId} myLabelId`);
980990
}));
981991

992+
it('should trim the custom panel aria-labelledby when there is no label', fakeAsync(() => {
993+
// Reset the `placeholder` which also controls the label of the form field.
994+
fixture.componentInstance.select.placeholder = '';
995+
fixture.componentInstance.ariaLabelledby = 'myLabelId';
996+
fixture.componentInstance.select.open();
997+
fixture.detectChanges();
998+
flush();
999+
1000+
// Note that we assert that there are no spaces around the value.
1001+
const panel = document.querySelector('.mat-select-panel')!;
1002+
expect(panel.getAttribute('aria-labelledby')).toBe(`myLabelId`);
1003+
}));
1004+
9821005
it('should clear aria-labelledby from the panel if an aria-label is set', fakeAsync(() => {
9831006
fixture.componentInstance.ariaLabel = 'My label';
9841007
fixture.componentInstance.select.open();

src/material/select/select.ts

+5-8
Original file line numberDiff line numberDiff line change
@@ -1004,8 +1004,9 @@ export abstract class _MatSelectBase<C> extends _MatSelectMixinBase implements A
10041004
return null;
10051005
}
10061006

1007-
const labelId = this._getLabelId();
1008-
return this.ariaLabelledby ? labelId + ' ' + this.ariaLabelledby : labelId;
1007+
const labelId = this._parentFormField?.getLabelId();
1008+
const labelExpression = (labelId ? labelId + ' ' : '');
1009+
return this.ariaLabelledby ? labelExpression + this.ariaLabelledby : labelId;
10091010
}
10101011

10111012
/** Determines the `aria-activedescendant` to be set on the host. */
@@ -1017,18 +1018,14 @@ export abstract class _MatSelectBase<C> extends _MatSelectMixinBase implements A
10171018
return null;
10181019
}
10191020

1020-
/** Gets the ID of the element that is labelling the select. */
1021-
private _getLabelId(): string {
1022-
return this._parentFormField?.getLabelId() || '';
1023-
}
1024-
10251021
/** Gets the aria-labelledby of the select component trigger. */
10261022
private _getTriggerAriaLabelledby(): string | null {
10271023
if (this.ariaLabel) {
10281024
return null;
10291025
}
10301026

1031-
let value = this._getLabelId() + ' ' + this._valueId;
1027+
const labelId = this._parentFormField?.getLabelId();
1028+
let value = (labelId ? labelId + ' ' : '') + this._valueId;
10321029

10331030
if (this.ariaLabelledby) {
10341031
value += ' ' + this.ariaLabelledby;

0 commit comments

Comments
 (0)