Skip to content

Commit cdbf876

Browse files
crisbetoandrewseguin
authored andcommitted
fix(material/select): trim aria-labelledby (#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 #22192. (cherry picked from commit 9b67d67)
1 parent 7acd695 commit cdbf876

File tree

3 files changed

+53
-9
lines changed

3 files changed

+53
-9
lines changed

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

Lines changed: 25 additions & 1 deletion
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();
@@ -3834,7 +3857,7 @@ describe('MDC-based MatSelect', () => {
38343857
template: `
38353858
<div [style.height.px]="heightAbove"></div>
38363859
<mat-form-field>
3837-
<mat-label>Select a food</mat-label>
3860+
<mat-label *ngIf="hasLabel">Select a food</mat-label>
38383861
<mat-select placeholder="Food" [formControl]="control" [required]="isRequired"
38393862
[tabIndex]="tabIndexOverride" [aria-label]="ariaLabel" [aria-labelledby]="ariaLabelledby"
38403863
[panelClass]="panelClass" [disableRipple]="disableRipple"
@@ -3862,6 +3885,7 @@ class BasicSelect {
38623885
isRequired: boolean;
38633886
heightAbove = 0;
38643887
heightBelow = 0;
3888+
hasLabel = true;
38653889
tabIndexOverride: number;
38663890
ariaLabel: string;
38673891
ariaLabelledby: string;

src/material/select/select.spec.ts

Lines changed: 23 additions & 0 deletions
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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,8 +1030,9 @@ export abstract class _MatSelectBase<C> extends _MatSelectMixinBase implements A
10301030
return null;
10311031
}
10321032

1033-
const labelId = this._getLabelId();
1034-
return this.ariaLabelledby ? labelId + ' ' + this.ariaLabelledby : labelId;
1033+
const labelId = this._parentFormField?.getLabelId();
1034+
const labelExpression = (labelId ? labelId + ' ' : '');
1035+
return this.ariaLabelledby ? labelExpression + this.ariaLabelledby : labelId;
10351036
}
10361037

10371038
/** Determines the `aria-activedescendant` to be set on the host. */
@@ -1043,18 +1044,14 @@ export abstract class _MatSelectBase<C> extends _MatSelectMixinBase implements A
10431044
return null;
10441045
}
10451046

1046-
/** Gets the ID of the element that is labelling the select. */
1047-
private _getLabelId(): string {
1048-
return this._parentFormField?.getLabelId() || '';
1049-
}
1050-
10511047
/** Gets the aria-labelledby of the select component trigger. */
10521048
private _getTriggerAriaLabelledby(): string | null {
10531049
if (this.ariaLabel) {
10541050
return null;
10551051
}
10561052

1057-
let value = this._getLabelId() + ' ' + this._valueId;
1053+
const labelId = this._parentFormField?.getLabelId();
1054+
let value = (labelId ? labelId + ' ' : '') + this._valueId;
10581055

10591056
if (this.ariaLabelledby) {
10601057
value += ' ' + this.ariaLabelledby;

0 commit comments

Comments
 (0)