Skip to content

Commit 13da794

Browse files
fix(material/input): Do not set aria-invalid on required empty matInputs
Updates the logic for setting `aria-invalid` on `matInput` to not set the attribute at all if the input is `required` and has no value. Prior to this PR `matInput` sets `aria-invalid="false"` for any empty `matInput`, including `required` ones. This suppresses screen readers' announcement to users that such inputs are in an invalid state. Fixes #22777
1 parent 0a4dbe1 commit 13da794

File tree

4 files changed

+112
-6
lines changed

4 files changed

+112
-6
lines changed

src/material-experimental/mdc-input/input.spec.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,7 +1081,7 @@ describe('MatMdcInput with forms', () => {
10811081
expect(describedBy).toBe(errorIds);
10821082
}));
10831083

1084-
it('should not set `aria-invalid` to true if the input is empty', fakeAsync(() => {
1084+
it('should set `aria-invalid` to true if the input is empty', fakeAsync(() => {
10851085
// Submit the form since it's the one that triggers the default error state matcher.
10861086
dispatchFakeEvent(fixture.nativeElement.querySelector('form'), 'submit');
10871087
fixture.detectChanges();
@@ -1090,7 +1090,7 @@ describe('MatMdcInput with forms', () => {
10901090
expect(testComponent.formControl.invalid).toBe(true, 'Expected form control to be invalid');
10911091
expect(inputEl.value).toBeFalsy();
10921092
expect(inputEl.getAttribute('aria-invalid'))
1093-
.toBe('false', 'Expected aria-invalid to be set to "false".');
1093+
.toBe('true', 'Expected aria-invalid to be set to "true"');
10941094

10951095
inputEl.value = 'not valid';
10961096
fixture.detectChanges();
@@ -1300,6 +1300,48 @@ describe('MatFormField default options', () => {
13001300

13011301
});
13021302

1303+
describe('MatInput that is requried with a formControl', () => {
1304+
let fixture: ComponentFixture<MatInputWithRequiredAndFormControl>;
1305+
let input: HTMLInputElement;
1306+
let formControl: FormControl;
1307+
1308+
beforeEach(() => {
1309+
fixture = createComponent(MatInputWithRequiredAndFormControl);
1310+
formControl = fixture.componentInstance.formControl;
1311+
input = fixture.debugElement.query(By.css('input')).nativeElement!;
1312+
fixture.detectChanges();
1313+
});
1314+
1315+
it('should set aria-required.', () => {
1316+
expect(input.getAttribute('aria-required'))
1317+
.toBe('true', 'Expected "aria-required" to be "true"');
1318+
});
1319+
1320+
it('should not set any value for aria-invalid.', () => {
1321+
expect(input.getAttribute('aria-invalid'))
1322+
.toBe(null, 'Expected "aria-invalid" not to be set');
1323+
});
1324+
1325+
it('should not set aria-invalid when empty and invalid.', () => {
1326+
formControl.setErrors({error: 'True!'});
1327+
formControl.markAsTouched();
1328+
fixture.detectChanges();
1329+
1330+
expect(input.getAttribute('aria-invalid'))
1331+
.toBe(null, 'Expected "aria-invalid" not to be set');
1332+
});
1333+
1334+
it('should set aria-invalid when not empty and invalid.', () => {
1335+
input.value = 'Some value';
1336+
formControl.setErrors({error: 'True!'});
1337+
formControl.markAsTouched();
1338+
fixture.detectChanges();
1339+
1340+
expect(input.getAttribute('aria-invalid'))
1341+
.toBe('true', 'Expected "aria-invalid" to be "true"');
1342+
});
1343+
});
1344+
13031345
function configureTestingModule(component: Type<any>, options:
13041346
{providers?: Provider[], imports?: any[], declarations?: any[], animations?: boolean} = {}) {
13051347
const {providers = [], imports = [], declarations = [], animations = true} = options;
@@ -1823,3 +1865,14 @@ class MatInputWithColor {
18231865
`
18241866
})
18251867
class MatInputInsideOutsideFormField {}
1868+
1869+
@Component({
1870+
template: `
1871+
<mat-form-field>
1872+
<input matInput required [formControl]="formControl">
1873+
</mat-form-field>
1874+
`
1875+
})
1876+
class MatInputWithRequiredAndFormControl {
1877+
formControl = new FormControl();
1878+
}

src/material-experimental/mdc-input/input.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import {MatInput as BaseMatInput} from '@angular/material/input';
3939
'[attr.readonly]': 'readonly && !_isNativeSelect || null',
4040
// Only mark the input as invalid for assistive technology if it has a value since the
4141
// state usually overlaps with `aria-required` when the input is empty and can be redundant.
42-
'[attr.aria-invalid]': 'errorState && !empty',
42+
'[attr.aria-invalid]': '(empty && required) ? null : errorState',
4343
'[attr.aria-required]': 'required',
4444
},
4545
providers: [{provide: MatFormFieldControl, useExisting: MatInput}],

src/material/input/input.spec.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,7 +1219,7 @@ describe('MatInput with forms', () => {
12191219
expect(describedBy).toBe(errorIds);
12201220
}));
12211221

1222-
it('should not set `aria-invalid` to true if the input is empty', fakeAsync(() => {
1222+
it('should set `aria-invalid` to true if the input is empty', fakeAsync(() => {
12231223
// Submit the form since it's the one that triggers the default error state matcher.
12241224
dispatchFakeEvent(fixture.nativeElement.querySelector('form'), 'submit');
12251225
fixture.detectChanges();
@@ -1228,7 +1228,7 @@ describe('MatInput with forms', () => {
12281228
expect(testComponent.formControl.invalid).toBe(true, 'Expected form control to be invalid');
12291229
expect(inputEl.value).toBeFalsy();
12301230
expect(inputEl.getAttribute('aria-invalid'))
1231-
.toBe('false', 'Expected aria-invalid to be set to "false".');
1231+
.toBe('true', 'Expected aria-invalid to be set to "true".');
12321232

12331233
inputEl.value = 'not valid';
12341234
fixture.detectChanges();
@@ -1754,6 +1754,48 @@ describe('MatInput with textarea autosize', () => {
17541754
});
17551755
});
17561756

1757+
describe('MatInput that is requried with a formControl', () => {
1758+
let fixture: ComponentFixture<MatInputWithRequiredAndFormControl>;
1759+
let input: HTMLInputElement;
1760+
let formControl: FormControl;
1761+
1762+
beforeEach(() => {
1763+
fixture = createComponent(MatInputWithRequiredAndFormControl);
1764+
formControl = fixture.componentInstance.formControl;
1765+
input = fixture.debugElement.query(By.css('input')).nativeElement!;
1766+
fixture.detectChanges();
1767+
});
1768+
1769+
it('should set aria-required.', () => {
1770+
expect(input.getAttribute('aria-required'))
1771+
.toBe('true', 'Expected "aria-required" to be "true"');
1772+
});
1773+
1774+
it('should not set any value for aria-invalid.', () => {
1775+
expect(input.getAttribute('aria-invalid'))
1776+
.toBe(null, 'Expected "aria-invalid" not to be set');
1777+
});
1778+
1779+
it('should not set aria-invalid when empty and invalid.', () => {
1780+
formControl.setErrors({error: 'True!'});
1781+
formControl.markAsTouched();
1782+
fixture.detectChanges();
1783+
1784+
expect(input.getAttribute('aria-invalid'))
1785+
.toBe(null, 'Expected "aria-invalid" not to be set');
1786+
});
1787+
1788+
it('should set aria-invalid when not empty and invalid.', () => {
1789+
input.value = 'Some value';
1790+
formControl.setErrors({error: 'True!'});
1791+
formControl.markAsTouched();
1792+
fixture.detectChanges();
1793+
1794+
expect(input.getAttribute('aria-invalid'))
1795+
.toBe('true', 'Expected "aria-invalid" to be "true"');
1796+
});
1797+
});
1798+
17571799

17581800
function createComponent<T>(component: Type<T>,
17591801
providers: Provider[] = [],
@@ -2385,3 +2427,14 @@ class MatInputWithAnotherNgIf {
23852427
class MatInputWithColor {
23862428
color: ThemePalette;
23872429
}
2430+
2431+
@Component({
2432+
template: `
2433+
<mat-form-field>
2434+
<input matInput required [formControl]="formControl">
2435+
</mat-form-field>
2436+
`
2437+
})
2438+
class MatInputWithRequiredAndFormControl {
2439+
formControl = new FormControl();
2440+
}

src/material/input/input.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ const _MatInputBase = mixinErrorState(class {
8383
'[attr.readonly]': 'readonly && !_isNativeSelect || null',
8484
// Only mark the input as invalid for assistive technology if it has a value since the
8585
// state usually overlaps with `aria-required` when the input is empty and can be redundant.
86-
'[attr.aria-invalid]': 'errorState && !empty',
86+
'[attr.aria-invalid]': '(empty && required) ? null : errorState',
8787
'[attr.aria-required]': 'required',
8888
},
8989
providers: [{provide: MatFormFieldControl, useExisting: MatInput}],

0 commit comments

Comments
 (0)