Skip to content

Commit 0c03946

Browse files
crisbetommalerba
authored andcommitted
fix(input): set aria-invalid on mdInput element (#4757)
* Sets the `aria-invalid` attribute on the `mdInput` node, depending on whether it is invalid. * Moves the `_isErrorState` logic to the input child to avoid a circular dependency.
1 parent be8d3b2 commit 0c03946

File tree

2 files changed

+32
-17
lines changed

2 files changed

+32
-17
lines changed

src/lib/input/input-container.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,17 +596,21 @@ describe('MdInputContainer', function () {
596596
let fixture: ComponentFixture<MdInputContainerWithFormErrorMessages>;
597597
let testComponent: MdInputContainerWithFormErrorMessages;
598598
let containerEl: HTMLElement;
599+
let inputEl: HTMLElement;
599600

600601
beforeEach(() => {
601602
fixture = TestBed.createComponent(MdInputContainerWithFormErrorMessages);
602603
fixture.detectChanges();
603604
testComponent = fixture.componentInstance;
604605
containerEl = fixture.debugElement.query(By.css('md-input-container')).nativeElement;
606+
inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
605607
});
606608

607609
it('should not show any errors if the user has not interacted', () => {
608610
expect(testComponent.formControl.untouched).toBe(true, 'Expected untouched form control');
609611
expect(containerEl.querySelectorAll('md-error').length).toBe(0, 'Expected no error messages');
612+
expect(inputEl.getAttribute('aria-invalid'))
613+
.toBe('false', 'Expected aria-invalid to be set to "false".');
610614
});
611615

612616
it('should display an error message when the input is touched and invalid', async(() => {
@@ -621,6 +625,8 @@ describe('MdInputContainer', function () {
621625
.toContain('mat-input-invalid', 'Expected container to have the invalid CSS class.');
622626
expect(containerEl.querySelectorAll('md-error').length)
623627
.toBe(1, 'Expected one error message to have been rendered.');
628+
expect(inputEl.getAttribute('aria-invalid'))
629+
.toBe('true', 'Expected aria-invalid to be set to "true".');
624630
});
625631
}));
626632

@@ -638,6 +644,8 @@ describe('MdInputContainer', function () {
638644
.toContain('mat-input-invalid', 'Expected container to have the invalid CSS class.');
639645
expect(containerEl.querySelectorAll('md-error').length)
640646
.toBe(1, 'Expected one error message to have been rendered.');
647+
expect(inputEl.getAttribute('aria-invalid'))
648+
.toBe('true', 'Expected aria-invalid to be set to "true".');
641649
});
642650
}));
643651

@@ -650,9 +658,12 @@ describe('MdInputContainer', function () {
650658
groupFixture.detectChanges();
651659
component = groupFixture.componentInstance;
652660
containerEl = groupFixture.debugElement.query(By.css('md-input-container')).nativeElement;
661+
inputEl = groupFixture.debugElement.query(By.css('input')).nativeElement;
653662

654663
expect(component.formGroup.invalid).toBe(true, 'Expected form control to be invalid');
655664
expect(containerEl.querySelectorAll('md-error').length).toBe(0, 'Expected no error messages');
665+
expect(inputEl.getAttribute('aria-invalid'))
666+
.toBe('false', 'Expected aria-invalid to be set to "false".');
656667
expect(component.formGroupDirective.submitted)
657668
.toBe(false, 'Expected form not to have been submitted');
658669

@@ -666,6 +677,8 @@ describe('MdInputContainer', function () {
666677
.toContain('mat-input-invalid', 'Expected container to have the invalid CSS class.');
667678
expect(containerEl.querySelectorAll('md-error').length)
668679
.toBe(1, 'Expected one error message to have been rendered.');
680+
expect(inputEl.getAttribute('aria-invalid'))
681+
.toBe('true', 'Expected aria-invalid to be set to "true".');
669682
});
670683
}));
671684

src/lib/input/input-container.ts

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export class MdSuffix {}
112112
'[disabled]': 'disabled',
113113
'[required]': 'required',
114114
'[attr.aria-describedby]': 'ariaDescribedby || null',
115+
'[attr.aria-invalid]': '_isErrorState()',
115116
'(blur)': '_onBlur()',
116117
'(focus)': '_onFocus()',
117118
'(input)': '_onInput()',
@@ -210,7 +211,9 @@ export class MdInputDirective {
210211

211212
constructor(private _elementRef: ElementRef,
212213
private _renderer: Renderer2,
213-
@Optional() @Self() public _ngControl: NgControl) {
214+
@Optional() @Self() public _ngControl: NgControl,
215+
@Optional() private _parentForm: NgForm,
216+
@Optional() private _parentFormGroup: FormGroupDirective) {
214217

215218
// Force setter to be called in case id was not specified.
216219
this.id = this.id;
@@ -233,6 +236,17 @@ export class MdInputDirective {
233236
// FormsModule or ReactiveFormsModule, because Angular forms also listens to input events.
234237
}
235238

239+
/** Whether the input is in an error state. */
240+
_isErrorState(): boolean {
241+
const control = this._ngControl;
242+
const isInvalid = control && control.invalid;
243+
const isTouched = control && control.touched;
244+
const isSubmitted = (this._parentFormGroup && this._parentFormGroup.submitted) ||
245+
(this._parentForm && this._parentForm.submitted);
246+
247+
return !!(isInvalid && (isTouched || isSubmitted));
248+
}
249+
236250
/** Make sure the input is a supported type. */
237251
private _validateType() {
238252
if (MD_INPUT_INVALID_TYPES.indexOf(this._type) !== -1) {
@@ -275,7 +289,7 @@ export class MdInputDirective {
275289
// Remove align attribute to prevent it from interfering with layout.
276290
'[attr.align]': 'null',
277291
'[class.mat-input-container]': 'true',
278-
'[class.mat-input-invalid]': '_isErrorState()',
292+
'[class.mat-input-invalid]': '_mdInputChild._isErrorState()',
279293
'[class.mat-focused]': '_mdInputChild.focused',
280294
'[class.ng-untouched]': '_shouldForward("untouched")',
281295
'[class.ng-touched]': '_shouldForward("touched")',
@@ -354,9 +368,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
354368

355369
constructor(
356370
public _elementRef: ElementRef,
357-
private _changeDetectorRef: ChangeDetectorRef,
358-
@Optional() private _parentForm: NgForm,
359-
@Optional() private _parentFormGroup: FormGroupDirective) { }
371+
private _changeDetectorRef: ChangeDetectorRef) { }
360372

361373
ngAfterContentInit() {
362374
this._validateInputChild();
@@ -390,20 +402,10 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
390402
/** Focuses the underlying input. */
391403
_focusInput() { this._mdInputChild.focus(); }
392404

393-
/** Whether the input container is in an error state. */
394-
_isErrorState(): boolean {
395-
const control = this._mdInputChild._ngControl;
396-
const isInvalid = control && control.invalid;
397-
const isTouched = control && control.touched;
398-
const isSubmitted = (this._parentFormGroup && this._parentFormGroup.submitted) ||
399-
(this._parentForm && this._parentForm.submitted);
400-
401-
return !!(isInvalid && (isTouched || isSubmitted));
402-
}
403-
404405
/** Determines whether to display hints or errors. */
405406
_getDisplayedMessages(): 'error' | 'hint' {
406-
return (this._errorChildren.length > 0 && this._isErrorState()) ? 'error' : 'hint';
407+
let input = this._mdInputChild;
408+
return (this._errorChildren.length > 0 && input._isErrorState()) ? 'error' : 'hint';
407409
}
408410

409411
/**

0 commit comments

Comments
 (0)