diff --git a/src/material-experimental/mdc-input/input.spec.ts b/src/material-experimental/mdc-input/input.spec.ts index 4d7a47f0a18b..4ac92e7593f3 100644 --- a/src/material-experimental/mdc-input/input.spec.ts +++ b/src/material-experimental/mdc-input/input.spec.ts @@ -1081,7 +1081,7 @@ describe('MatMdcInput with forms', () => { expect(describedBy).toBe(errorIds); })); - it('should not set `aria-invalid` to true if the input is empty', fakeAsync(() => { + it('should set `aria-invalid` to true if the input is empty', fakeAsync(() => { // Submit the form since it's the one that triggers the default error state matcher. dispatchFakeEvent(fixture.nativeElement.querySelector('form'), 'submit'); fixture.detectChanges(); @@ -1090,7 +1090,7 @@ describe('MatMdcInput with forms', () => { expect(testComponent.formControl.invalid).toBe(true, 'Expected form control to be invalid'); expect(inputEl.value).toBeFalsy(); expect(inputEl.getAttribute('aria-invalid')) - .toBe('false', 'Expected aria-invalid to be set to "false".'); + .toBe('true', 'Expected aria-invalid to be set to "true"'); inputEl.value = 'not valid'; fixture.detectChanges(); @@ -1300,6 +1300,48 @@ describe('MatFormField default options', () => { }); +describe('MatInput that is requried with a formControl', () => { + let fixture: ComponentFixture; + let input: HTMLInputElement; + let formControl: FormControl; + + beforeEach(() => { + fixture = createComponent(MatInputWithRequiredAndFormControl); + formControl = fixture.componentInstance.formControl; + input = fixture.debugElement.query(By.css('input')).nativeElement!; + fixture.detectChanges(); + }); + + it('should set aria-required.', () => { + expect(input.getAttribute('aria-required')) + .toBe('true', 'Expected "aria-required" to be "true"'); + }); + + it('should not set any value for aria-invalid.', () => { + expect(input.getAttribute('aria-invalid')) + .toBe(null, 'Expected "aria-invalid" not to be set'); + }); + + it('should not set aria-invalid when empty and invalid.', () => { + formControl.setErrors({error: 'True!'}); + formControl.markAsTouched(); + fixture.detectChanges(); + + expect(input.getAttribute('aria-invalid')) + .toBe(null, 'Expected "aria-invalid" not to be set'); + }); + + it('should set aria-invalid when not empty and invalid.', () => { + input.value = 'Some value'; + formControl.setErrors({error: 'True!'}); + formControl.markAsTouched(); + fixture.detectChanges(); + + expect(input.getAttribute('aria-invalid')) + .toBe('true', 'Expected "aria-invalid" to be "true"'); + }); +}); + function configureTestingModule(component: Type, options: {providers?: Provider[], imports?: any[], declarations?: any[], animations?: boolean} = {}) { const {providers = [], imports = [], declarations = [], animations = true} = options; @@ -1823,3 +1865,14 @@ class MatInputWithColor { ` }) class MatInputInsideOutsideFormField {} + +@Component({ + template: ` + + + + ` +}) +class MatInputWithRequiredAndFormControl { + formControl = new FormControl(); +} diff --git a/src/material-experimental/mdc-input/input.ts b/src/material-experimental/mdc-input/input.ts index c16577810517..329d386fc21a 100644 --- a/src/material-experimental/mdc-input/input.ts +++ b/src/material-experimental/mdc-input/input.ts @@ -39,7 +39,7 @@ import {MatInput as BaseMatInput} from '@angular/material/input'; '[attr.readonly]': 'readonly && !_isNativeSelect || null', // Only mark the input as invalid for assistive technology if it has a value since the // state usually overlaps with `aria-required` when the input is empty and can be redundant. - '[attr.aria-invalid]': 'errorState && !empty', + '[attr.aria-invalid]': '(empty && required) ? null : errorState', '[attr.aria-required]': 'required', }, providers: [{provide: MatFormFieldControl, useExisting: MatInput}], diff --git a/src/material/input/input.spec.ts b/src/material/input/input.spec.ts index 96695bec2f98..67f67c0073df 100644 --- a/src/material/input/input.spec.ts +++ b/src/material/input/input.spec.ts @@ -1219,7 +1219,7 @@ describe('MatInput with forms', () => { expect(describedBy).toBe(errorIds); })); - it('should not set `aria-invalid` to true if the input is empty', fakeAsync(() => { + it('should set `aria-invalid` to true if the input is empty', fakeAsync(() => { // Submit the form since it's the one that triggers the default error state matcher. dispatchFakeEvent(fixture.nativeElement.querySelector('form'), 'submit'); fixture.detectChanges(); @@ -1228,7 +1228,7 @@ describe('MatInput with forms', () => { expect(testComponent.formControl.invalid).toBe(true, 'Expected form control to be invalid'); expect(inputEl.value).toBeFalsy(); expect(inputEl.getAttribute('aria-invalid')) - .toBe('false', 'Expected aria-invalid to be set to "false".'); + .toBe('true', 'Expected aria-invalid to be set to "true".'); inputEl.value = 'not valid'; fixture.detectChanges(); @@ -1754,6 +1754,48 @@ describe('MatInput with textarea autosize', () => { }); }); +describe('MatInput that is requried with a formControl', () => { + let fixture: ComponentFixture; + let input: HTMLInputElement; + let formControl: FormControl; + + beforeEach(() => { + fixture = createComponent(MatInputWithRequiredAndFormControl); + formControl = fixture.componentInstance.formControl; + input = fixture.debugElement.query(By.css('input')).nativeElement!; + fixture.detectChanges(); + }); + + it('should set aria-required.', () => { + expect(input.getAttribute('aria-required')) + .toBe('true', 'Expected "aria-required" to be "true"'); + }); + + it('should not set any value for aria-invalid.', () => { + expect(input.getAttribute('aria-invalid')) + .toBe(null, 'Expected "aria-invalid" not to be set'); + }); + + it('should not set aria-invalid when empty and invalid.', () => { + formControl.setErrors({error: 'True!'}); + formControl.markAsTouched(); + fixture.detectChanges(); + + expect(input.getAttribute('aria-invalid')) + .toBe(null, 'Expected "aria-invalid" not to be set'); + }); + + it('should set aria-invalid when not empty and invalid.', () => { + input.value = 'Some value'; + formControl.setErrors({error: 'True!'}); + formControl.markAsTouched(); + fixture.detectChanges(); + + expect(input.getAttribute('aria-invalid')) + .toBe('true', 'Expected "aria-invalid" to be "true"'); + }); +}); + function createComponent(component: Type, providers: Provider[] = [], @@ -2385,3 +2427,14 @@ class MatInputWithAnotherNgIf { class MatInputWithColor { color: ThemePalette; } + +@Component({ + template: ` + + + + ` +}) +class MatInputWithRequiredAndFormControl { + formControl = new FormControl(); +} diff --git a/src/material/input/input.ts b/src/material/input/input.ts index 7a56680b6843..3443a23681d9 100644 --- a/src/material/input/input.ts +++ b/src/material/input/input.ts @@ -83,7 +83,7 @@ const _MatInputBase = mixinErrorState(class { '[attr.readonly]': 'readonly && !_isNativeSelect || null', // Only mark the input as invalid for assistive technology if it has a value since the // state usually overlaps with `aria-required` when the input is empty and can be redundant. - '[attr.aria-invalid]': 'errorState && !empty', + '[attr.aria-invalid]': '(empty && required) ? null : errorState', '[attr.aria-required]': 'required', }, providers: [{provide: MatFormFieldControl, useExisting: MatInput}],