From d3d416308b814b9d4518024639602a0f35f30fee Mon Sep 17 00:00:00 2001
From: Andrew Seguin <andrewjs@google.com>
Date: Thu, 23 Jan 2025 16:02:48 -0700
Subject: [PATCH] fix(material/form-field): move error aria-live to parent
 container

---
 CHANGELOG.md                                  |  2 +-
 src/material/chips/chip-grid.spec.ts          |  4 +++-
 src/material/form-field/directives/error.ts   | 21 ++-----------------
 src/material/form-field/form-field.html       | 12 +++++++----
 src/material/form-field/form-field.ts         |  6 +++---
 src/material/input/input.spec.ts              |  6 ++++--
 tools/public_api_guard/material/form-field.md |  2 +-
 7 files changed, 22 insertions(+), 31 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78e52f967c7c..30303b0e353b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5557,4 +5557,4 @@ _Breaking changes:_
 
 # Changes Prior to 12.0.0
 
-To view changes that occurred prior to 12.0.0, see [CHANGELOG_ARCHIVE.md](https://github.com/angular/components/blob/main/CHANGELOG_ARCHIVE.md).
\ No newline at end of file
+To view changes that occurred prior to 12.0.0, see [CHANGELOG_ARCHIVE.md](https://github.com/angular/components/blob/main/CHANGELOG_ARCHIVE.md).
diff --git a/src/material/chips/chip-grid.spec.ts b/src/material/chips/chip-grid.spec.ts
index a4a67b84961b..7bf77e5dd96f 100644
--- a/src/material/chips/chip-grid.spec.ts
+++ b/src/material/chips/chip-grid.spec.ts
@@ -1003,7 +1003,9 @@ describe('MatChipGrid', () => {
       errorTestComponent.formControl.markAsTouched();
       fixture.detectChanges();
 
-      expect(containerEl.querySelector('mat-error')!.getAttribute('aria-live')).toBe('polite');
+      expect(
+        containerEl.querySelector('[aria-live]:has(mat-error)')!.getAttribute('aria-live'),
+      ).toBe('polite');
     });
 
     it('sets the aria-describedby on the input to reference errors when in error state', fakeAsync(() => {
diff --git a/src/material/form-field/directives/error.ts b/src/material/form-field/directives/error.ts
index a304e90a8171..9d474d478ad8 100644
--- a/src/material/form-field/directives/error.ts
+++ b/src/material/form-field/directives/error.ts
@@ -6,14 +6,7 @@
  * found in the LICENSE file at https://angular.dev/license
  */
 
-import {
-  Directive,
-  ElementRef,
-  InjectionToken,
-  Input,
-  HostAttributeToken,
-  inject,
-} from '@angular/core';
+import {Directive, InjectionToken, Input, inject} from '@angular/core';
 import {_IdGenerator} from '@angular/cdk/a11y';
 
 /**
@@ -28,7 +21,6 @@ export const MAT_ERROR = new InjectionToken<MatError>('MatError');
   selector: 'mat-error, [matError]',
   host: {
     'class': 'mat-mdc-form-field-error mat-mdc-form-field-bottom-align',
-    'aria-atomic': 'true',
     '[id]': 'id',
   },
   providers: [{provide: MAT_ERROR, useExisting: MatError}],
@@ -38,14 +30,5 @@ export class MatError {
 
   constructor(...args: unknown[]);
 
-  constructor() {
-    const ariaLive = inject(new HostAttributeToken('aria-live'), {optional: true});
-
-    // If no aria-live value is set add 'polite' as a default. This is preferred over setting
-    // role='alert' so that screen readers do not interrupt the current task to read this aloud.
-    if (!ariaLive) {
-      const elementRef = inject(ElementRef);
-      elementRef.nativeElement.setAttribute('aria-live', 'polite');
-    }
-  }
+  constructor() {}
 }
diff --git a/src/material/form-field/form-field.html b/src/material/form-field/form-field.html
index f1a46075b46b..584625044757 100644
--- a/src/material/form-field/form-field.html
+++ b/src/material/form-field/form-field.html
@@ -99,14 +99,18 @@
   class="mat-mdc-form-field-subscript-wrapper mat-mdc-form-field-bottom-align"
   [class.mat-mdc-form-field-subscript-dynamic-size]="subscriptSizing === 'dynamic'"
 >
-  @switch (_getDisplayedMessages()) {
-    @case ('error') {
+  @let subscriptMessageType = _getSubscriptMessageType();
+
+  <div aria-atomic="true" aria-live="polite">
+    @if (subscriptMessageType === 'error') {
       <div class="mat-mdc-form-field-error-wrapper">
         <ng-content select="mat-error, [matError]"></ng-content>
       </div>
     }
+  </div>
 
-    @case ('hint') {
+  <div aria-atomic="true" aria-live="polite">
+    @if (subscriptMessageType === 'hint') {
       <div class="mat-mdc-form-field-hint-wrapper">
         @if (hintLabel) {
           <mat-hint [id]="_hintLabelId">{{hintLabel}}</mat-hint>
@@ -116,5 +120,5 @@
         <ng-content select="mat-hint[align='end']"></ng-content>
       </div>
     }
-  }
+  </div>
 </div>
diff --git a/src/material/form-field/form-field.ts b/src/material/form-field/form-field.ts
index 0ea473c78d41..d7ef992f763d 100644
--- a/src/material/form-field/form-field.ts
+++ b/src/material/form-field/form-field.ts
@@ -597,8 +597,8 @@ export class MatFormField
     return control && control[prop];
   }
 
-  /** Determines whether to display hints or errors. */
-  _getDisplayedMessages(): 'error' | 'hint' {
+  /** Gets the type of subscript message to render (error or hint). */
+  _getSubscriptMessageType(): 'error' | 'hint' {
     return this._errorChildren && this._errorChildren.length > 0 && this._control.errorState
       ? 'error'
       : 'hint';
@@ -666,7 +666,7 @@ export class MatFormField
         ids.push(...this._control.userAriaDescribedBy.split(' '));
       }
 
-      if (this._getDisplayedMessages() === 'hint') {
+      if (this._getSubscriptMessageType() === 'hint') {
         const startHint = this._hintChildren
           ? this._hintChildren.find(hint => hint.align === 'start')
           : null;
diff --git a/src/material/input/input.spec.ts b/src/material/input/input.spec.ts
index 3a778acbcee4..df3118007c33 100644
--- a/src/material/input/input.spec.ts
+++ b/src/material/input/input.spec.ts
@@ -1229,11 +1229,13 @@ describe('MatMdcInput with forms', () => {
         .toBe(1);
     }));
 
-    it('should set the proper aria-live attribute on the error messages', fakeAsync(() => {
+    it('should be in a parent element with the an aria-live attribute to announce the error', fakeAsync(() => {
       testComponent.formControl.markAsTouched();
       fixture.detectChanges();
 
-      expect(containerEl.querySelector('mat-error')!.getAttribute('aria-live')).toBe('polite');
+      expect(
+        containerEl.querySelector('[aria-live]:has(mat-error)')!.getAttribute('aria-live'),
+      ).toBe('polite');
     }));
 
     it('sets the aria-describedby to reference errors when in error state', fakeAsync(() => {
diff --git a/tools/public_api_guard/material/form-field.md b/tools/public_api_guard/material/form-field.md
index 539f205be870..ccec71731263 100644
--- a/tools/public_api_guard/material/form-field.md
+++ b/tools/public_api_guard/material/form-field.md
@@ -83,8 +83,8 @@ export class MatFormField implements FloatingLabelParent, AfterContentInit, Afte
     // (undocumented)
     _formFieldControl: MatFormFieldControl_2<any>;
     getConnectedOverlayOrigin(): ElementRef;
-    _getDisplayedMessages(): 'error' | 'hint';
     getLabelId: Signal<string | null>;
+    _getSubscriptMessageType(): 'error' | 'hint';
     _handleLabelResized(): void;
     // (undocumented)
     _hasFloatingLabel: Signal<boolean>;