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') { -
- @if (hintLabel) { - {{hintLabel}} - } - -
- -
+
+ +
+ +
+ @if (hintLabel) { + {{hintLabel}} } - } + +
+ +
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();