Skip to content

Commit edfd8dc

Browse files
committed
fix(material/form-field): preserve aria-describedby set externally across all form controls
add describedbyids and use to preserve existing ids add describedByIds to input add comment Move better sync logic to formfield add describedByIds to other controls
1 parent 1b4cae7 commit edfd8dc

File tree

8 files changed

+75
-22
lines changed

8 files changed

+75
-22
lines changed

src/material/chips/chip-grid.ts

+8
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,14 @@ export class MatChipGrid
349349
this.stateChanges.next();
350350
}
351351

352+
/**
353+
* Implemented as part of MatFormFieldControl.
354+
* @docs-private
355+
*/
356+
get describedByIds(): string[] {
357+
return this._chipInput?.describedByIds || [];
358+
}
359+
352360
/**
353361
* Implemented as part of MatFormFieldControl.
354362
* @docs-private

src/material/chips/chip-input.ts

+11
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,17 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy {
207207
this.inputElement.value = '';
208208
}
209209

210+
/**
211+
* Implemented as part of MatChipTextControl.
212+
* @docs-private
213+
*/
214+
get describedByIds(): string[] {
215+
const element = this._elementRef.nativeElement;
216+
const existingDescribedBy = element.getAttribute('aria-describedby');
217+
218+
return existingDescribedBy?.split(' ') || [];
219+
}
220+
210221
setDescribedByIds(ids: string[]): void {
211222
const element = this._elementRef.nativeElement;
212223

src/material/chips/chip-text-control.ts

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ export interface MatChipTextControl {
2323
/** Focuses the text control. */
2424
focus(): void;
2525

26+
/** Gets the list of ids the input is described by. */
27+
readonly describedByIds?: string[];
28+
2629
/** Sets the list of ids the input is described by. */
2730
setDescribedByIds(ids: string[]): void;
2831
}

src/material/datepicker/date-range-input.ts

+8
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,14 @@ export class MatDateRangeInput<D>
284284
this.ngControl = inject(ControlContainer, {optional: true, self: true}) as any;
285285
}
286286

287+
/**
288+
* Implemented as part of MatFormFieldControl.
289+
* @docs-private
290+
*/
291+
get describedByIds(): string[] {
292+
return this._ariaDescribedBy?.split(' ') || [];
293+
}
294+
287295
/**
288296
* Implemented as a part of `MatFormFieldControl`.
289297
* @docs-private

src/material/form-field/form-field-control.ts

+3
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ export abstract class MatFormFieldControl<T> {
7575
*/
7676
readonly disableAutomaticLabeling?: boolean;
7777

78+
/** Gets the list of element IDs that currently describe this control. */
79+
readonly describedByIds?: string[];
80+
7881
/** Sets the list of element IDs that currently describe this control. */
7982
abstract setDescribedByIds(ids: string[]): void;
8083

src/material/form-field/form-field.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,9 @@ export class MatFormField
308308
// Unique id for the hint label.
309309
readonly _hintLabelId = this._idGenerator.getId('mat-mdc-hint-');
310310

311+
// Ids obtained from the fields
312+
private _describedByIds: string[] | undefined;
313+
311314
/** Gets the current form field control */
312315
get _control(): MatFormFieldControl<any> {
313316
return this._explicitFormFieldControl || this._formFieldControl;
@@ -705,7 +708,22 @@ export class MatFormField
705708
ids.push(...this._errorChildren.map(error => error.id));
706709
}
707710

708-
this._control.setDescribedByIds(ids);
711+
const existingDescribedBy = this._control.describedByIds;
712+
let toAssign: string[];
713+
714+
// In some cases there might be some `aria-describedby` IDs that were assigned directly,
715+
// like by the `AriaDescriber` (see #30011). Attempt to preserve them by taking the previous
716+
// attribute value and filtering out the IDs that came from the previous `setDescribedByIds`
717+
// call. Note the `|| ids` here allows us to avoid duplicating IDs on the first render.
718+
if (existingDescribedBy) {
719+
const exclude = this._describedByIds || ids;
720+
toAssign = ids.concat(existingDescribedBy.filter(id => id && !exclude.includes(id)));
721+
} else {
722+
toAssign = ids;
723+
}
724+
725+
this._control.setDescribedByIds(toAssign);
726+
this._describedByIds = ids;
709727
}
710728
}
711729

src/material/input/input.ts

+12-21
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,6 @@ export class MatInput
114114
private _cleanupIosKeyup: (() => void) | undefined;
115115
private _cleanupWebkitWheel: (() => void) | undefined;
116116

117-
/** `aria-describedby` IDs assigned by the form field. */
118-
private _formFieldDescribedBy: string[] | undefined;
119-
120117
/** Whether the component is being rendered on the server. */
121118
readonly _isServer: boolean;
122119

@@ -554,28 +551,22 @@ export class MatInput
554551
* Implemented as part of MatFormFieldControl.
555552
* @docs-private
556553
*/
557-
setDescribedByIds(ids: string[]) {
554+
get describedByIds(): string[] {
558555
const element = this._elementRef.nativeElement;
559556
const existingDescribedBy = element.getAttribute('aria-describedby');
560-
let toAssign: string[];
561-
562-
// In some cases there might be some `aria-describedby` IDs that were assigned directly,
563-
// like by the `AriaDescriber` (see #30011). Attempt to preserve them by taking the previous
564-
// attribute value and filtering out the IDs that came from the previous `setDescribedByIds`
565-
// call. Note the `|| ids` here allows us to avoid duplicating IDs on the first render.
566-
if (existingDescribedBy) {
567-
const exclude = this._formFieldDescribedBy || ids;
568-
toAssign = ids.concat(
569-
existingDescribedBy.split(' ').filter(id => id && !exclude.includes(id)),
570-
);
571-
} else {
572-
toAssign = ids;
573-
}
574557

575-
this._formFieldDescribedBy = ids;
558+
return existingDescribedBy?.split(' ') || [];
559+
}
560+
561+
/**
562+
* Implemented as part of MatFormFieldControl.
563+
* @docs-private
564+
*/
565+
setDescribedByIds(ids: string[]) {
566+
const element = this._elementRef.nativeElement;
576567

577-
if (toAssign.length) {
578-
element.setAttribute('aria-describedby', toAssign.join(' '));
568+
if (ids.length) {
569+
element.setAttribute('aria-describedby', ids.join(' '));
579570
} else {
580571
element.removeAttribute('aria-describedby');
581572
}

src/material/select/select.ts

+11
Original file line numberDiff line numberDiff line change
@@ -1449,6 +1449,17 @@ export class MatSelect
14491449
return value;
14501450
}
14511451

1452+
/**
1453+
* Implemented as part of MatFormFieldControl.
1454+
* @docs-private
1455+
*/
1456+
get describedByIds(): string[] {
1457+
const element = this._elementRef.nativeElement;
1458+
const existingDescribedBy = element.getAttribute('aria-describedby');
1459+
1460+
return existingDescribedBy?.split(' ') || [];
1461+
}
1462+
14521463
/**
14531464
* Implemented as part of MatFormFieldControl.
14541465
* @docs-private

0 commit comments

Comments
 (0)