Skip to content

Commit e9b0157

Browse files
authored
fix(material/form-field): move error aria-live to parent container (#30678)
fix(material/form-field): move error aria-live to parent container restructure errors Use single wrapper with aria-live update specs fix error update api goldens Add comment for new div
1 parent 5d27e7c commit e9b0157

File tree

8 files changed

+34
-39
lines changed

8 files changed

+34
-39
lines changed

goldens/material/form-field/index.api.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ export class MatFormField implements FloatingLabelParent, AfterContentInit, Afte
8080
// (undocumented)
8181
_formFieldControl: MatFormFieldControl_2<any>;
8282
getConnectedOverlayOrigin(): ElementRef;
83-
_getDisplayedMessages(): 'error' | 'hint';
8483
getLabelId: i0.Signal<string | null>;
84+
_getSubscriptMessageType(): 'error' | 'hint';
8585
_handleLabelResized(): void;
8686
// (undocumented)
8787
_hasFloatingLabel: i0.Signal<boolean>;

goldens/material/input/index.api.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ export class MatFormField implements FloatingLabelParent, AfterContentInit, Afte
7373
// (undocumented)
7474
_formFieldControl: MatFormFieldControl<any>;
7575
getConnectedOverlayOrigin(): ElementRef;
76-
_getDisplayedMessages(): 'error' | 'hint';
7776
getLabelId: i0.Signal<string | null>;
77+
_getSubscriptMessageType(): 'error' | 'hint';
7878
_handleLabelResized(): void;
7979
// (undocumented)
8080
_hasFloatingLabel: i0.Signal<boolean>;

goldens/material/select/index.api.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@ export class MatFormField implements FloatingLabelParent, AfterContentInit, Afte
9696
// (undocumented)
9797
_formFieldControl: MatFormFieldControl_2<any>;
9898
getConnectedOverlayOrigin(): ElementRef;
99-
_getDisplayedMessages(): 'error' | 'hint';
10099
getLabelId: i0.Signal<string | null>;
100+
_getSubscriptMessageType(): 'error' | 'hint';
101101
_handleLabelResized(): void;
102102
// (undocumented)
103103
_hasFloatingLabel: i0.Signal<boolean>;

src/material/chips/chip-grid.spec.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -983,7 +983,9 @@ describe('MatChipGrid', () => {
983983
errorTestComponent.formControl.markAsTouched();
984984
fixture.detectChanges();
985985

986-
expect(containerEl.querySelector('mat-error')!.getAttribute('aria-live')).toBe('polite');
986+
expect(
987+
containerEl.querySelector('[aria-live]:has(mat-error)')!.getAttribute('aria-live'),
988+
).toBe('polite');
987989
});
988990

989991
it('sets the aria-describedby on the input to reference errors when in error state', fakeAsync(() => {

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

+2-19
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {
10-
Directive,
11-
ElementRef,
12-
InjectionToken,
13-
Input,
14-
HostAttributeToken,
15-
inject,
16-
} from '@angular/core';
9+
import {Directive, InjectionToken, Input, inject} from '@angular/core';
1710
import {_IdGenerator} from '@angular/cdk/a11y';
1811

1912
/**
@@ -28,7 +21,6 @@ export const MAT_ERROR = new InjectionToken<MatError>('MatError');
2821
selector: 'mat-error, [matError]',
2922
host: {
3023
'class': 'mat-mdc-form-field-error mat-mdc-form-field-bottom-align',
31-
'aria-atomic': 'true',
3224
'[id]': 'id',
3325
},
3426
providers: [{provide: MAT_ERROR, useExisting: MatError}],
@@ -38,14 +30,5 @@ export class MatError {
3830

3931
constructor(...args: unknown[]);
4032

41-
constructor() {
42-
const ariaLive = inject(new HostAttributeToken('aria-live'), {optional: true});
43-
44-
// If no aria-live value is set add 'polite' as a default. This is preferred over setting
45-
// role='alert' so that screen readers do not interrupt the current task to read this aloud.
46-
if (!ariaLive) {
47-
const elementRef = inject(ElementRef);
48-
elementRef.nativeElement.setAttribute('aria-live', 'polite');
49-
}
50-
}
33+
constructor() {}
5134
}

src/material/form-field/form-field.html

+19-11
Original file line numberDiff line numberDiff line change
@@ -96,25 +96,33 @@
9696
</div>
9797

9898
<div
99-
class="mat-mdc-form-field-subscript-wrapper mat-mdc-form-field-bottom-align"
100-
[class.mat-mdc-form-field-subscript-dynamic-size]="subscriptSizing === 'dynamic'"
99+
class="mat-mdc-form-field-subscript-wrapper mat-mdc-form-field-bottom-align"
100+
[class.mat-mdc-form-field-subscript-dynamic-size]="subscriptSizing === 'dynamic'"
101101
>
102-
@switch (_getDisplayedMessages()) {
103-
@case ('error') {
104-
<div class="mat-mdc-form-field-error-wrapper">
102+
@let subscriptMessageType = _getSubscriptMessageType();
103+
104+
<!--
105+
Use a single permanent wrapper for both hints and errors so aria-live works correctly,
106+
as having it appear post render will not consistently work. We also do not want to add
107+
additional divs as it causes styling regressions.
108+
-->
109+
<div aria-atomic="true" aria-live="polite"
110+
[class.mat-mdc-form-field-error-wrapper]="subscriptMessageType === 'error'"
111+
[class.mat-mdc-form-field-hint-wrapper]="subscriptMessageType === 'hint'"
112+
>
113+
@switch (subscriptMessageType) {
114+
@case ('error') {
105115
<ng-content select="mat-error, [matError]"></ng-content>
106-
</div>
107-
}
116+
}
108117

109-
@case ('hint') {
110-
<div class="mat-mdc-form-field-hint-wrapper">
118+
@case ('hint') {
111119
@if (hintLabel) {
112120
<mat-hint [id]="_hintLabelId">{{hintLabel}}</mat-hint>
113121
}
114122
<ng-content select="mat-hint:not([align='end'])"></ng-content>
115123
<div class="mat-mdc-form-field-hint-spacer"></div>
116124
<ng-content select="mat-hint[align='end']"></ng-content>
117-
</div>
125+
}
118126
}
119-
}
127+
</div>
120128
</div>

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -627,8 +627,8 @@ export class MatFormField
627627
return control && control[prop];
628628
}
629629

630-
/** Determines whether to display hints or errors. */
631-
_getDisplayedMessages(): 'error' | 'hint' {
630+
/** Gets the type of subscript message to render (error or hint). */
631+
_getSubscriptMessageType(): 'error' | 'hint' {
632632
return this._errorChildren && this._errorChildren.length > 0 && this._control.errorState
633633
? 'error'
634634
: 'hint';
@@ -696,7 +696,7 @@ export class MatFormField
696696
ids.push(...this._control.userAriaDescribedBy.split(' '));
697697
}
698698

699-
if (this._getDisplayedMessages() === 'hint') {
699+
if (this._getSubscriptMessageType() === 'hint') {
700700
const startHint = this._hintChildren
701701
? this._hintChildren.find(hint => hint.align === 'start')
702702
: null;

src/material/input/input.spec.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1266,11 +1266,13 @@ describe('MatMdcInput with forms', () => {
12661266
.toBe(1);
12671267
}));
12681268

1269-
it('should set the proper aria-live attribute on the error messages', fakeAsync(() => {
1269+
it('should be in a parent element with the an aria-live attribute to announce the error', fakeAsync(() => {
12701270
testComponent.formControl.markAsTouched();
12711271
fixture.detectChanges();
12721272

1273-
expect(containerEl.querySelector('mat-error')!.getAttribute('aria-live')).toBe('polite');
1273+
expect(
1274+
containerEl.querySelector('[aria-live]:has(mat-error)')!.getAttribute('aria-live'),
1275+
).toBe('polite');
12741276
}));
12751277

12761278
it('sets the aria-describedby to reference errors when in error state', fakeAsync(() => {

0 commit comments

Comments
 (0)