Skip to content

Commit d0c53ac

Browse files
authored
fix(material/input): only set as aria-invalid if the input isn't empty (#21609)
Only sets `aria-invalid` on a `MatInput` if it is invalid and it has a value, otherwise it'll likely overlap with `aria-required` and cause more noise for users. Furthermore, it may be confusing if the user lands on an input that they haven't interacted with, but it's already invalid. Fixes #18140.
1 parent cfbbbfe commit d0c53ac

File tree

4 files changed

+64
-14
lines changed

4 files changed

+64
-14
lines changed

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

+28-5
Original file line numberDiff line numberDiff line change
@@ -896,7 +896,7 @@ describe('MatMdcInput with forms', () => {
896896
let fixture: ComponentFixture<MatInputWithFormErrorMessages>;
897897
let testComponent: MatInputWithFormErrorMessages;
898898
let containerEl: HTMLElement;
899-
let inputEl: HTMLElement;
899+
let inputEl: HTMLInputElement;
900900

901901
beforeEach(fakeAsync(() => {
902902
fixture = createComponent(MatInputWithFormErrorMessages);
@@ -917,6 +917,7 @@ describe('MatMdcInput with forms', () => {
917917
expect(testComponent.formControl.invalid).toBe(true, 'Expected form control to be invalid');
918918
expect(containerEl.querySelectorAll('mat-error').length).toBe(0, 'Expected no error message');
919919

920+
inputEl.value = 'not valid';
920921
testComponent.formControl.markAsTouched();
921922
fixture.detectChanges();
922923
flush();
@@ -949,6 +950,7 @@ describe('MatMdcInput with forms', () => {
949950
expect(testComponent.formControl.invalid).toBe(true, 'Expected form control to be invalid');
950951
expect(containerEl.querySelectorAll('mat-error').length).toBe(0, 'Expected no error message');
951952

953+
inputEl.value = 'not valid';
952954
dispatchFakeEvent(fixture.debugElement.query(By.css('form'))!.nativeElement, 'submit');
953955
fixture.detectChanges();
954956
flush();
@@ -981,6 +983,7 @@ describe('MatMdcInput with forms', () => {
981983
expect(component.formGroupDirective.submitted)
982984
.toBe(false, 'Expected form not to have been submitted');
983985

986+
inputEl.value = 'not valid';
984987
dispatchFakeEvent(groupFixture.debugElement.query(By.css('form'))!.nativeElement, 'submit');
985988
groupFixture.detectChanges();
986989
flush();
@@ -1007,7 +1010,7 @@ describe('MatMdcInput with forms', () => {
10071010
expect(containerEl.querySelectorAll('mat-hint').length)
10081011
.toBe(0, 'Expected no hints to be shown.');
10091012

1010-
testComponent.formControl.setValue('something');
1013+
testComponent.formControl.setValue('valid value');
10111014
fixture.detectChanges();
10121015
flush();
10131016

@@ -1059,6 +1062,26 @@ describe('MatMdcInput with forms', () => {
10591062
expect(errorIds).toBeTruthy('errors should be shown');
10601063
expect(describedBy).toBe(errorIds);
10611064
}));
1065+
1066+
it('should not set `aria-invalid` to true if the input is empty', fakeAsync(() => {
1067+
// Submit the form since it's the one that triggers the default error state matcher.
1068+
dispatchFakeEvent(fixture.nativeElement.querySelector('form'), 'submit');
1069+
fixture.detectChanges();
1070+
flush();
1071+
1072+
expect(testComponent.formControl.invalid).toBe(true, 'Expected form control to be invalid');
1073+
expect(inputEl.value).toBeFalsy();
1074+
expect(inputEl.getAttribute('aria-invalid'))
1075+
.toBe('false', 'Expected aria-invalid to be set to "false".');
1076+
1077+
inputEl.value = 'not valid';
1078+
fixture.detectChanges();
1079+
1080+
expect(testComponent.formControl.invalid).toBe(true, 'Expected form control to be invalid');
1081+
expect(inputEl.getAttribute('aria-invalid'))
1082+
.toBe('true', 'Expected aria-invalid to be set to "true".');
1083+
}));
1084+
10621085
});
10631086

10641087
describe('custom error behavior', () => {
@@ -1524,7 +1547,7 @@ class MatInputMissingMatInputTestController {}
15241547
})
15251548
class MatInputWithFormErrorMessages {
15261549
@ViewChild('form') form: NgForm;
1527-
formControl = new FormControl('', Validators.required);
1550+
formControl = new FormControl('', [Validators.required, Validators.pattern(/valid value/)]);
15281551
renderError = true;
15291552
}
15301553

@@ -1543,7 +1566,7 @@ class MatInputWithFormErrorMessages {
15431566
})
15441567
class MatInputWithCustomErrorStateMatcher {
15451568
formGroup = new FormGroup({
1546-
name: new FormControl('', Validators.required)
1569+
name: new FormControl('', [Validators.required, Validators.pattern(/valid value/)])
15471570
});
15481571

15491572
errorState = false;
@@ -1567,7 +1590,7 @@ class MatInputWithCustomErrorStateMatcher {
15671590
class MatInputWithFormGroupErrorMessages {
15681591
@ViewChild(FormGroupDirective) formGroupDirective: FormGroupDirective;
15691592
formGroup = new FormGroup({
1570-
name: new FormControl('', Validators.required)
1593+
name: new FormControl('', [Validators.required, Validators.pattern(/valid value/)])
15711594
});
15721595
}
15731596

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ import {MatInput as BaseMatInput} from '@angular/material/input';
3333
'[required]': 'required',
3434
'[attr.placeholder]': 'placeholder',
3535
'[attr.readonly]': 'readonly && !_isNativeSelect || null',
36-
'[attr.aria-invalid]': 'errorState',
37-
'[attr.aria-required]': 'required.toString()',
36+
// Only mark the input as invalid for assistive technology if it has a value since the
37+
// state usually overlaps with `aria-required` when the input is empty and can be redundant.
38+
'[attr.aria-invalid]': 'errorState && !empty',
39+
'[attr.aria-required]': 'required',
3840
},
3941
providers: [{provide: MatFormFieldControl, useExisting: MatInput}],
4042
})

src/material/input/input.spec.ts

+28-5
Original file line numberDiff line numberDiff line change
@@ -1067,7 +1067,7 @@ describe('MatInput with forms', () => {
10671067
let fixture: ComponentFixture<MatInputWithFormErrorMessages>;
10681068
let testComponent: MatInputWithFormErrorMessages;
10691069
let containerEl: HTMLElement;
1070-
let inputEl: HTMLElement;
1070+
let inputEl: HTMLInputElement;
10711071

10721072
beforeEach(fakeAsync(() => {
10731073
fixture = createComponent(MatInputWithFormErrorMessages);
@@ -1088,6 +1088,7 @@ describe('MatInput with forms', () => {
10881088
expect(testComponent.formControl.invalid).toBe(true, 'Expected form control to be invalid');
10891089
expect(containerEl.querySelectorAll('mat-error').length).toBe(0, 'Expected no error message');
10901090

1091+
inputEl.value = 'not valid';
10911092
testComponent.formControl.markAsTouched();
10921093
fixture.detectChanges();
10931094
flush();
@@ -1105,6 +1106,7 @@ describe('MatInput with forms', () => {
11051106
expect(testComponent.formControl.invalid).toBe(true, 'Expected form control to be invalid');
11061107
expect(containerEl.querySelectorAll('mat-error').length).toBe(0, 'Expected no error message');
11071108

1109+
inputEl.value = 'not valid';
11081110
dispatchFakeEvent(fixture.debugElement.query(By.css('form'))!.nativeElement, 'submit');
11091111
fixture.detectChanges();
11101112
flush();
@@ -1137,6 +1139,7 @@ describe('MatInput with forms', () => {
11371139
expect(component.formGroupDirective.submitted)
11381140
.toBe(false, 'Expected form not to have been submitted');
11391141

1142+
inputEl.value = 'not valid';
11401143
dispatchFakeEvent(groupFixture.debugElement.query(By.css('form'))!.nativeElement, 'submit');
11411144
groupFixture.detectChanges();
11421145
flush();
@@ -1163,7 +1166,7 @@ describe('MatInput with forms', () => {
11631166
expect(containerEl.querySelectorAll('mat-hint').length)
11641167
.toBe(0, 'Expected no hints to be shown.');
11651168

1166-
testComponent.formControl.setValue('something');
1169+
testComponent.formControl.setValue('valid value');
11671170
fixture.detectChanges();
11681171
flush();
11691172

@@ -1215,6 +1218,26 @@ describe('MatInput with forms', () => {
12151218
expect(errorIds).toBeTruthy('errors should be shown');
12161219
expect(describedBy).toBe(errorIds);
12171220
}));
1221+
1222+
it('should not set `aria-invalid` to true if the input is empty', fakeAsync(() => {
1223+
// Submit the form since it's the one that triggers the default error state matcher.
1224+
dispatchFakeEvent(fixture.nativeElement.querySelector('form'), 'submit');
1225+
fixture.detectChanges();
1226+
flush();
1227+
1228+
expect(testComponent.formControl.invalid).toBe(true, 'Expected form control to be invalid');
1229+
expect(inputEl.value).toBeFalsy();
1230+
expect(inputEl.getAttribute('aria-invalid'))
1231+
.toBe('false', 'Expected aria-invalid to be set to "false".');
1232+
1233+
inputEl.value = 'not valid';
1234+
fixture.detectChanges();
1235+
1236+
expect(testComponent.formControl.invalid).toBe(true, 'Expected form control to be invalid');
1237+
expect(inputEl.getAttribute('aria-invalid'))
1238+
.toBe('true', 'Expected aria-invalid to be set to "true".');
1239+
}));
1240+
12181241
});
12191242

12201243
describe('custom error behavior', () => {
@@ -2005,7 +2028,7 @@ class MatInputMissingMatInputTestController {}
20052028
})
20062029
class MatInputWithFormErrorMessages {
20072030
@ViewChild('form') form: NgForm;
2008-
formControl = new FormControl('', Validators.required);
2031+
formControl = new FormControl('', [Validators.required, Validators.pattern(/valid value/)]);
20092032
renderError = true;
20102033
}
20112034

@@ -2024,7 +2047,7 @@ class MatInputWithFormErrorMessages {
20242047
})
20252048
class MatInputWithCustomErrorStateMatcher {
20262049
formGroup = new FormGroup({
2027-
name: new FormControl('', Validators.required)
2050+
name: new FormControl('', [Validators.required, Validators.pattern(/valid value/)])
20282051
});
20292052

20302053
errorState = false;
@@ -2048,7 +2071,7 @@ class MatInputWithCustomErrorStateMatcher {
20482071
class MatInputWithFormGroupErrorMessages {
20492072
@ViewChild(FormGroupDirective) formGroupDirective: FormGroupDirective;
20502073
formGroup = new FormGroup({
2051-
name: new FormControl('', Validators.required)
2074+
name: new FormControl('', [Validators.required, Validators.pattern(/valid value/)])
20522075
});
20532076
}
20542077

src/material/input/input.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,10 @@ const _MatInputMixinBase: CanUpdateErrorStateCtor & typeof MatInputBase =
8484
'[disabled]': 'disabled',
8585
'[required]': 'required',
8686
'[attr.readonly]': 'readonly && !_isNativeSelect || null',
87-
'[attr.aria-invalid]': 'errorState',
88-
'[attr.aria-required]': 'required.toString()',
87+
// Only mark the input as invalid for assistive technology if it has a value since the
88+
// state usually overlaps with `aria-required` when the input is empty and can be redundant.
89+
'[attr.aria-invalid]': 'errorState && !empty',
90+
'[attr.aria-required]': 'required',
8991
},
9092
providers: [{provide: MatFormFieldControl, useExisting: MatInput}],
9193
})

0 commit comments

Comments
 (0)