Skip to content

Commit 5ba9792

Browse files
committedNov 14, 2024
fix(material/form-field): avoid touching the DOM on each state change
Currently we set the `aria-describedby` every time the state of the form control changes. This is excessive, because it only needs to happen if the error state or `userAriaDescribedBy` change. (cherry picked from commit d62c236)
1 parent 13beab5 commit 5ba9792

File tree

2 files changed

+20
-4
lines changed

2 files changed

+20
-4
lines changed
 

Diff for: ‎src/material/form-field/form-field.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import {AbstractControlDirective} from '@angular/forms';
3636
import {ThemePalette} from '@angular/material/core';
3737
import {_IdGenerator} from '@angular/cdk/a11y';
3838
import {Subject, Subscription, merge} from 'rxjs';
39-
import {takeUntil} from 'rxjs/operators';
39+
import {map, pairwise, takeUntil, filter, startWith} from 'rxjs/operators';
4040
import {MAT_ERROR, MatError} from './directives/error';
4141
import {
4242
FLOATING_LABEL_PARENT,
@@ -328,6 +328,7 @@ export class MatFormField
328328
private _previousControl: MatFormFieldControl<unknown> | null = null;
329329
private _stateChanges: Subscription | undefined;
330330
private _valueChanges: Subscription | undefined;
331+
private _describedByChanges: Subscription | undefined;
331332

332333
private _injector = inject(Injector);
333334

@@ -377,6 +378,7 @@ export class MatFormField
377378
ngOnDestroy() {
378379
this._stateChanges?.unsubscribe();
379380
this._valueChanges?.unsubscribe();
381+
this._describedByChanges?.unsubscribe();
380382
this._destroyed.next();
381383
this._destroyed.complete();
382384
}
@@ -426,10 +428,22 @@ export class MatFormField
426428
this._stateChanges?.unsubscribe();
427429
this._stateChanges = control.stateChanges.subscribe(() => {
428430
this._updateFocusState();
429-
this._syncDescribedByIds();
430431
this._changeDetectorRef.markForCheck();
431432
});
432433

434+
// Updating the `aria-describedby` touches the DOM. Only do it if it actually needs to change.
435+
this._describedByChanges?.unsubscribe();
436+
this._describedByChanges = control.stateChanges
437+
.pipe(
438+
startWith([undefined, undefined] as const),
439+
map(() => [control.errorState, control.userAriaDescribedBy] as const),
440+
pairwise(),
441+
filter(([[prevErrorState, prevDescribedBy], [currentErrorState, currentDescribedBy]]) => {
442+
return prevErrorState !== currentErrorState || prevDescribedBy !== currentDescribedBy;
443+
}),
444+
)
445+
.subscribe(() => this._syncDescribedByIds());
446+
433447
this._valueChanges?.unsubscribe();
434448

435449
// Run change detection if the value changes.

Diff for: ‎src/material/input/input.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -551,10 +551,12 @@ export class MatInput
551551
* @docs-private
552552
*/
553553
setDescribedByIds(ids: string[]) {
554+
const element = this._elementRef.nativeElement;
555+
554556
if (ids.length) {
555-
this._elementRef.nativeElement.setAttribute('aria-describedby', ids.join(' '));
557+
element.setAttribute('aria-describedby', ids.join(' '));
556558
} else {
557-
this._elementRef.nativeElement.removeAttribute('aria-describedby');
559+
element.removeAttribute('aria-describedby');
558560
}
559561
}
560562

0 commit comments

Comments
 (0)
Please sign in to comment.