From a878e8427647397623e1dc10e2b14240336df1f8 Mon Sep 17 00:00:00 2001 From: DBowen33 Date: Wed, 11 Sep 2024 20:59:36 +0000 Subject: [PATCH 1/2] fix(material/form-field): error text fix fixed issue where certain screen reader and browser pairings do not recognize mat-error being inserted into the DOM, changed it to where visibility is changing on the wrapper rather than adding and removing the hint and error wrappers. the changing of visibility should be recognized by all browser and screen reader pairings Fixes #29616 --- src/material/form-field/form-field.html | 37 ++++++++++----------- src/material/form-field/form-field.scss | 8 +++++ src/material/form-field/form-field.ts | 43 +++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 20 deletions(-) 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..3b122b2d303d 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(); } @@ -682,6 +686,45 @@ 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', + ); + 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', + ); + } + } + /** * 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 From e1fc95220c8517b1410db383c11f86c93c91393b Mon Sep 17 00:00:00 2001 From: DBowen33 Date: Thu, 12 Sep 2024 20:51:01 +0000 Subject: [PATCH 2/2] fix(material/form-field): add tests add tests fixes #29721 --- src/material/chips/chip-grid.spec.ts | 32 +++++------ src/material/form-field/form-field.ts | 11 ++++ src/material/input/input.spec.ts | 80 +++++++++++++-------------- src/material/select/select.spec.ts | 4 +- 4 files changed, 69 insertions(+), 58 deletions(-) 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.ts b/src/material/form-field/form-field.ts index 3b122b2d303d..f583e01bdf9e 100644 --- a/src/material/form-field/form-field.ts +++ b/src/material/form-field/form-field.ts @@ -662,6 +662,7 @@ export class MatFormField } if (this._getDisplayedMessages() === 'hint') { + this._showOrHideSubscript(); const startHint = this._hintChildren ? this._hintChildren.find(hint => hint.align === 'start') : null; @@ -705,6 +706,10 @@ export class MatFormField 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; } @@ -723,6 +728,12 @@ export class MatFormField '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', + ); + } } /** 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();