Skip to content

Commit b09e2c6

Browse files
committed
fix(material/form-field): move error aria-live to parent container
1 parent 4a0818d commit b09e2c6

File tree

6 files changed

+25
-34
lines changed

6 files changed

+25
-34
lines changed

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -1003,7 +1003,9 @@ describe('MatChipGrid', () => {
10031003
errorTestComponent.formControl.markAsTouched();
10041004
fixture.detectChanges();
10051005

1006-
expect(containerEl.querySelector('mat-error')!.getAttribute('aria-live')).toBe('polite');
1006+
expect(
1007+
containerEl.querySelector('[aria-live]:has(mat-error)')!.getAttribute('aria-live'),
1008+
).toBe('polite');
10071009
});
10081010

10091011
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

+12-8
Original file line numberDiff line numberDiff line change
@@ -96,20 +96,24 @@
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') {
102+
@let subscriptMessageType = _getSubscriptMessageType();
103+
104+
<div aria-atomic="true" aria-live="polite">
105+
@if (subscriptMessageType == 'error') {
104106
<div
105-
class="mat-mdc-form-field-error-wrapper"
106-
[@transitionMessages]="_subscriptAnimationState"
107+
class="mat-mdc-form-field-error-wrapper"
108+
[@transitionMessages]="_subscriptAnimationState"
107109
>
108110
<ng-content select="mat-error, [matError]"></ng-content>
109111
</div>
110112
}
113+
</div>
111114

112-
@case ('hint') {
115+
<div aria-atomic="true" aria-live="polite">
116+
@if (subscriptMessageType == 'hint') {
113117
<div class="mat-mdc-form-field-hint-wrapper" [@transitionMessages]="_subscriptAnimationState">
114118
@if (hintLabel) {
115119
<mat-hint [id]="_hintLabelId">{{hintLabel}}</mat-hint>
@@ -119,5 +123,5 @@
119123
<ng-content select="mat-hint[align='end']"></ng-content>
120124
</div>
121125
}
122-
}
126+
</div>
123127
</div>

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -592,8 +592,8 @@ export class MatFormField
592592
return control && control[prop];
593593
}
594594

595-
/** Determines whether to display hints or errors. */
596-
_getDisplayedMessages(): 'error' | 'hint' {
595+
/** Gets the type of subscript message to render (error or hint). */
596+
_getSubscriptMessageType(): 'error' | 'hint' {
597597
return this._errorChildren && this._errorChildren.length > 0 && this._control.errorState
598598
? 'error'
599599
: 'hint';
@@ -661,7 +661,7 @@ export class MatFormField
661661
ids.push(...this._control.userAriaDescribedBy.split(' '));
662662
}
663663

664-
if (this._getDisplayedMessages() === 'hint') {
664+
if (this._getSubscriptMessageType() === 'hint') {
665665
const startHint = this._hintChildren
666666
? this._hintChildren.find(hint => hint.align === 'start')
667667
: null;

src/material/input/input.spec.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1238,11 +1238,13 @@ describe('MatMdcInput with forms', () => {
12381238
.toBe(1);
12391239
}));
12401240

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

1245-
expect(containerEl.querySelector('mat-error')!.getAttribute('aria-live')).toBe('polite');
1245+
expect(
1246+
containerEl.querySelector('[aria-live]:has(mat-error)')!.getAttribute('aria-live'),
1247+
).toBe('polite');
12461248
}));
12471249

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

tools/public_api_guard/material/form-field.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ export class MatFormField implements FloatingLabelParent, AfterContentInit, Afte
8383
// (undocumented)
8484
_formFieldControl: MatFormFieldControl_2<any>;
8585
getConnectedOverlayOrigin(): ElementRef;
86-
_getDisplayedMessages(): 'error' | 'hint';
8786
getLabelId: Signal<string | null>;
87+
_getSubscriptMessageType(): 'error' | 'hint';
8888
_handleLabelResized(): void;
8989
// (undocumented)
9090
_hasFloatingLabel: Signal<boolean>;

0 commit comments

Comments
 (0)