Skip to content

Commit 9db29c9

Browse files
committed
fix(material/input): preserve aria-describedby set externally
Currently there are two sources of an `aria-describedby` for a `matInput`: the IDs of the hint/error message in the form field and any custom ones set through `aria-describedby`. This is insufficient, because the ID can also come from a direct DOM manupulation like in the `AriaDescriber`. These changes tweak the logic to try and preserve them, because currently they get overwritten. Fixes angular#30011.
1 parent d62c236 commit 9db29c9

File tree

2 files changed

+34
-2
lines changed

2 files changed

+34
-2
lines changed

src/material/input/input.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,18 @@ describe('MatMdcInput without forms', () => {
643643
expect(input.getAttribute('aria-describedby')).toBe('start end');
644644
}));
645645

646+
it('should preserve aria-describedby set directly in the DOM', fakeAsync(() => {
647+
const fixture = createComponent(MatInputHintLabel2TestController);
648+
const input = fixture.nativeElement.querySelector('input');
649+
input.setAttribute('aria-describedby', 'custom');
650+
fixture.componentInstance.label = 'label';
651+
fixture.changeDetectorRef.markForCheck();
652+
fixture.detectChanges();
653+
const hint = fixture.nativeElement.querySelector('.mat-mdc-form-field-hint');
654+
655+
expect(input.getAttribute('aria-describedby')).toBe(`${hint.getAttribute('id')} custom`);
656+
}));
657+
646658
it('should set a class on the hint element based on its alignment', fakeAsync(() => {
647659
const fixture = createComponent(MatInputMultipleHintTestController);
648660

src/material/input/input.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ export class MatInput
111111
private _webkitBlinkWheelListenerAttached = false;
112112
private _config = inject(MAT_INPUT_CONFIG, {optional: true});
113113

114+
/** `aria-describedby` IDs assigned by the form field. */
115+
private _formFieldDescribedBy: string[] | undefined;
116+
114117
/** Whether the component is being rendered on the server. */
115118
readonly _isServer: boolean;
116119

@@ -552,9 +555,26 @@ export class MatInput
552555
*/
553556
setDescribedByIds(ids: string[]) {
554557
const element = this._elementRef.nativeElement;
558+
const existingDescribedBy = element.getAttribute('aria-describedby');
559+
let toAssign: string[];
560+
561+
// In some cases there might be some `aria-describedby` IDs that were assigned directly,
562+
// like by the `AriaDescriber` (see #30011). Attempt to preserve them by taking the previous
563+
// attribute value and filtering out the IDs that came from the previous `setDescribedByIds`
564+
// call. Note the `|| ids` here allows us to avoid duplicating IDs on the first render.
565+
if (existingDescribedBy) {
566+
const exclude = this._formFieldDescribedBy || ids;
567+
toAssign = ids.concat(
568+
existingDescribedBy.split(' ').filter(id => id && !exclude.includes(id)),
569+
);
570+
} else {
571+
toAssign = ids;
572+
}
573+
574+
this._formFieldDescribedBy = ids;
555575

556-
if (ids.length) {
557-
element.setAttribute('aria-describedby', ids.join(' '));
576+
if (toAssign.length) {
577+
element.setAttribute('aria-describedby', toAssign.join(' '));
558578
} else {
559579
element.removeAttribute('aria-describedby');
560580
}

0 commit comments

Comments
 (0)