diff --git a/src/material/chips/chip-grid.spec.ts b/src/material/chips/chip-grid.spec.ts
index c78f3771818f..fdddc3325989 100644
--- a/src/material/chips/chip-grid.spec.ts
+++ b/src/material/chips/chip-grid.spec.ts
@@ -899,9 +899,9 @@ describe('MatChipGrid', () => {
expect(errorTestComponent.formControl.untouched)
.withContext('Expected untouched form control')
.toBe(true);
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected no error message')
- .toBe(0);
+ .toBe(1);
expect(chipGridEl.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to "false".')
.toBe('false');
@@ -911,9 +911,9 @@ describe('MatChipGrid', () => {
expect(errorTestComponent.formControl.invalid)
.withContext('Expected form control to be invalid')
.toBe(true);
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected no error message')
- .toBe(0);
+ .toBe(1);
errorTestComponent.formControl.markAsTouched();
fixture.detectChanges();
@@ -922,9 +922,9 @@ describe('MatChipGrid', () => {
expect(containerEl.classList)
.withContext('Expected container to have the invalid CSS class.')
.toContain('mat-form-field-invalid');
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected one error message to have been rendered.')
- .toBe(1);
+ .toBe(0);
expect(chipGridEl.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to "true".')
.toBe('true');
@@ -937,9 +937,9 @@ describe('MatChipGrid', () => {
expect(errorTestComponent.formControl.invalid)
.withContext('Expected form control to be invalid')
.toBe(true);
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected no error message')
- .toBe(0);
+ .toBe(1);
dispatchFakeEvent(fixture.debugElement.query(By.css('form'))!.nativeElement, 'submit');
flush();
@@ -952,9 +952,9 @@ describe('MatChipGrid', () => {
expect(containerEl.classList)
.withContext('Expected container to have the invalid CSS class.')
.toContain('mat-form-field-invalid');
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected one error message to have been rendered.')
- .toBe(1);
+ .toBe(0);
expect(chipGridEl.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to "true".')
.toBe('true');
@@ -971,12 +971,12 @@ describe('MatChipGrid', () => {
expect(containerEl.classList)
.withContext('Expected container to have the invalid CSS class.')
.toContain('mat-form-field-invalid');
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected one error message to have been rendered.')
- .toBe(1);
- expect(containerEl.querySelectorAll('mat-hint').length)
- .withContext('Expected no hints to be shown.')
.toBe(0);
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-hint-wrapper--hidden').length)
+ .withContext('Expected no hints to be shown.')
+ .toBe(1);
errorTestComponent.formControl.setValue('something');
flush();
@@ -987,9 +987,9 @@ describe('MatChipGrid', () => {
'mat-form-field-invalid',
'Expected container not to have the invalid class when valid.',
);
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected no error messages when the input is valid.')
- .toBe(0);
+ .toBe(1);
expect(containerEl.querySelectorAll('mat-hint').length)
.withContext('Expected one hint to be shown once the input is valid.')
.toBe(1);
diff --git a/src/material/form-field/form-field.html b/src/material/form-field/form-field.html
index 0c62f7ca809f..1d1c7003f5ac 100644
--- a/src/material/form-field/form-field.html
+++ b/src/material/form-field/form-field.html
@@ -99,25 +99,22 @@
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') {
-
-
-
- }
-
- @case ('hint') {
-
+
+
+
+
+
diff --git a/src/material/form-field/form-field.scss b/src/material/form-field/form-field.scss
index d796ecb06e1a..1d7d8af0667e 100644
--- a/src/material/form-field/form-field.scss
+++ b/src/material/form-field/form-field.scss
@@ -218,3 +218,11 @@ $_icon-prefix-infix-padding: 4px;
.mdc-notched-outline--upgraded .mdc-floating-label--float-above {
max-width: calc(100% * 4 / 3 + 1px);
}
+
+.mat-mdc-form-field-error-wrapper--hidden {
+ visibility: hidden;
+}
+
+.mat-mdc-form-field-hint-wrapper--hidden {
+ visibility: hidden;
+}
diff --git a/src/material/form-field/form-field.ts b/src/material/form-field/form-field.ts
index 00ae130f1399..f583e01bdf9e 100644
--- a/src/material/form-field/form-field.ts
+++ b/src/material/form-field/form-field.ts
@@ -437,6 +437,7 @@ export class MatFormField
this._stateChanges = control.stateChanges.subscribe(() => {
this._updateFocusState();
this._syncDescribedByIds();
+ this._showOrHideSubscript();
this._changeDetectorRef.markForCheck();
});
@@ -478,17 +479,20 @@ export class MatFormField
// Re-validate when the number of hints changes.
this._hintChildren.changes.subscribe(() => {
this._processHints();
+ this._showOrHideSubscript();
this._changeDetectorRef.markForCheck();
});
// Update the aria-described by when the number of errors changes.
this._errorChildren.changes.subscribe(() => {
this._syncDescribedByIds();
+ this._showOrHideSubscript();
this._changeDetectorRef.markForCheck();
});
// Initial mat-hint validation and subscript describedByIds sync.
this._validateHints();
+ this._showOrHideSubscript();
this._syncDescribedByIds();
}
@@ -658,6 +662,7 @@ export class MatFormField
}
if (this._getDisplayedMessages() === 'hint') {
+ this._showOrHideSubscript();
const startHint = this._hintChildren
? this._hintChildren.find(hint => hint.align === 'start')
: null;
@@ -682,6 +687,55 @@ export class MatFormField
}
}
+ /**
+ * Solves https://github.com/angular/components/issues/29616
+ * Issues with certain browser and screen reader pairings not able to announce mat-error
+ * when it's added to the DOM rather than changing the visibility of the hint/error wrappers.
+ * Changing visibility instead of adding the div wrappers works for all browsers and sreen
+ * readers.
+ *
+ * If there is an 'error' or 'hint' message being returned, remove visibility: hidden
+ * style class and show error or hint section of code. If no 'error' or 'hint' messages are
+ * being returned and no error children showing in query list, add visibility: hidden
+ * style class back to error wrapper.
+ */
+ private _showOrHideSubscript() {
+ switch (this._getDisplayedMessages()) {
+ case 'error': {
+ console.log(this._elementRef.nativeElement.children[1].children[0].classList);
+ this._elementRef.nativeElement.children[1].children[0].classList.remove(
+ 'mat-mdc-form-field-error-wrapper--hidden',
+ );
+ // Can't show error message and hint at same time
+ this._elementRef.nativeElement.children[1].children[0].classList.add(
+ 'mat-mdc-form-field-hint-wrapper--hidden',
+ );
+ console.log(this._elementRef.nativeElement.children[1].children[0].classList);
+ break;
+ }
+ case 'hint': {
+ console.log(this._elementRef.nativeElement.children[1].children[1].classList);
+ this._elementRef.nativeElement.children[1].children[1].classList.remove(
+ 'mat-mdc-form-field-hint-wrapper--hidden',
+ );
+ console.log(this._elementRef.nativeElement.children[1].children[1].classList);
+ break;
+ }
+ }
+
+ if (!this._errorChildren || this._errorChildren.length === 0 || !this._control.errorState) {
+ this._elementRef.nativeElement.children[1].children[0].classList.add(
+ 'mat-mdc-form-field-error-wrapper--hidden',
+ );
+ }
+
+ if (!this._hintChildren) {
+ this._elementRef.nativeElement.children[1].children[1].classList.add(
+ 'mat-mdc-form-field-hint-wrapper--hidden',
+ );
+ }
+ }
+
/**
* Updates the horizontal offset of the label in the outline appearance. In the outline
* appearance, the notched-outline and label are not relative to the infix container because
diff --git a/src/material/input/input.spec.ts b/src/material/input/input.spec.ts
index 13c2b1d4f45c..efd35ad392df 100644
--- a/src/material/input/input.spec.ts
+++ b/src/material/input/input.spec.ts
@@ -1056,9 +1056,9 @@ describe('MatMdcInput with forms', () => {
expect(testComponent.formControl.untouched)
.withContext('Expected untouched form control')
.toBe(true);
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected no error message')
- .toBe(0);
+ .toBe(1);
expect(inputEl.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to "false".')
.toBe('false');
@@ -1068,9 +1068,9 @@ describe('MatMdcInput with forms', () => {
expect(testComponent.formControl.invalid)
.withContext('Expected form control to be invalid')
.toBe(true);
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected no error message')
- .toBe(0);
+ .toBe(1);
inputEl.value = 'not valid';
testComponent.formControl.markAsTouched();
@@ -1080,9 +1080,9 @@ describe('MatMdcInput with forms', () => {
expect(containerEl.classList)
.withContext('Expected container to have the invalid CSS class.')
.toContain('mat-form-field-invalid');
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected one error message to have been rendered.')
- .toBe(1);
+ .toBe(0);
expect(inputEl.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to "true".')
.toBe('true');
@@ -1110,9 +1110,9 @@ describe('MatMdcInput with forms', () => {
expect(testComponent.formControl.invalid)
.withContext('Expected form control to be invalid')
.toBe(true);
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected no error message')
- .toBe(0);
+ .toBe(1);
inputEl.value = 'not valid';
dispatchFakeEvent(fixture.debugElement.query(By.css('form'))!.nativeElement, 'submit');
@@ -1125,9 +1125,9 @@ describe('MatMdcInput with forms', () => {
expect(containerEl.classList)
.withContext('Expected container to have the invalid CSS class.')
.toContain('mat-form-field-invalid');
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected one error message to have been rendered.')
- .toBe(1);
+ .toBe(0);
expect(inputEl.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to "true".')
.toBe('true');
@@ -1148,9 +1148,9 @@ describe('MatMdcInput with forms', () => {
expect(component.formGroup.invalid)
.withContext('Expected form control to be invalid')
.toBe(true);
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected no error message')
- .toBe(0);
+ .toBe(1);
expect(inputEl.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to "false".')
.toBe('false');
@@ -1169,9 +1169,9 @@ describe('MatMdcInput with forms', () => {
expect(containerEl.classList)
.withContext('Expected container to have the invalid CSS class.')
.toContain('mat-form-field-invalid');
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected one error message to have been rendered.')
- .toBe(1);
+ .toBe(0);
expect(inputEl.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to "true".')
.toBe('true');
@@ -1185,12 +1185,12 @@ describe('MatMdcInput with forms', () => {
expect(containerEl.classList)
.withContext('Expected container to have the invalid CSS class.')
.toContain('mat-form-field-invalid');
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected one error message to have been rendered.')
- .toBe(1);
- expect(containerEl.querySelectorAll('mat-hint').length)
- .withContext('Expected no hints to be shown.')
.toBe(0);
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-hint-wrapper--hidden').length)
+ .withContext('Expected no hints to be shown.')
+ .toBe(1);
testComponent.formControl.setValue('valid value');
fixture.detectChanges();
@@ -1200,12 +1200,12 @@ describe('MatMdcInput with forms', () => {
'mat-form-field-invalid',
'Expected container not to have the invalid class when valid.',
);
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected no error messages when the input is valid.')
- .toBe(0);
- expect(containerEl.querySelectorAll('mat-hint').length)
- .withContext('Expected one hint to be shown once the input is valid.')
.toBe(1);
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-hint-wrapper--hidden').length)
+ .withContext('Expected one hint to be shown once the input is valid.')
+ .toBe(0);
}));
it('should not hide the hint if there are no error messages', fakeAsync(() => {
@@ -1213,17 +1213,17 @@ describe('MatMdcInput with forms', () => {
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
- expect(containerEl.querySelectorAll('mat-hint').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-hint-wrapper--hidden').length)
.withContext('Expected one hint to be shown on load.')
- .toBe(1);
+ .toBe(0);
testComponent.formControl.markAsTouched();
fixture.detectChanges();
flush();
- expect(containerEl.querySelectorAll('mat-hint').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-hint-wrapper--hidden').length)
.withContext('Expected one hint to still be shown.')
- .toBe(1);
+ .toBe(0);
}));
it('should set the proper aria-live attribute on the error messages', fakeAsync(() => {
@@ -1292,24 +1292,24 @@ describe('MatMdcInput with forms', () => {
const control = component.formGroup.get('name')!;
expect(control.invalid).withContext('Expected form control to be invalid').toBe(true);
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected no error messages')
- .toBe(0);
+ .toBe(1);
control.markAsTouched();
fixture.detectChanges();
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected no error messages after being touched.')
- .toBe(0);
+ .toBe(1);
component.errorState = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected one error messages to have been rendered.')
- .toBe(1);
+ .toBe(0);
}));
it('should display an error message when global error matcher returns true', fakeAsync(() => {
@@ -1330,9 +1330,9 @@ describe('MatMdcInput with forms', () => {
expect(testComponent.formControl.untouched)
.withContext('Expected untouched form control')
.toBe(true);
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected an error message')
- .toBe(1);
+ .toBe(0);
}));
it('should display an error message when using ShowOnDirtyErrorStateMatcher', fakeAsync(() => {
@@ -1350,23 +1350,23 @@ describe('MatMdcInput with forms', () => {
expect(testComponent.formControl.invalid)
.withContext('Expected form control to be invalid')
.toBe(true);
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected no error message')
- .toBe(0);
+ .toBe(1);
testComponent.formControl.markAsTouched();
fixture.detectChanges();
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected no error messages when touched')
- .toBe(0);
+ .toBe(1);
testComponent.formControl.markAsDirty();
fixture.detectChanges();
- expect(containerEl.querySelectorAll('mat-error').length)
+ expect(containerEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected one error message when dirty')
- .toBe(1);
+ .toBe(0);
}));
});
diff --git a/src/material/select/select.spec.ts b/src/material/select/select.spec.ts
index c0a5dc64cec7..20e30c31fd1b 100644
--- a/src/material/select/select.spec.ts
+++ b/src/material/select/select.spec.ts
@@ -3257,9 +3257,9 @@ describe('MatSelect', () => {
it('should render the error messages when the parent form is submitted', fakeAsync(() => {
const debugEl = fixture.debugElement.nativeElement;
- expect(debugEl.querySelectorAll('mat-error').length)
+ expect(debugEl.querySelectorAll('.mat-mdc-form-field-error-wrapper--hidden').length)
.withContext('Expected no error messages')
- .toBe(0);
+ .toBe(1);
dispatchFakeEvent(fixture.debugElement.query(By.css('form'))!.nativeElement, 'submit');
fixture.detectChanges();