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.
This commit is contained in:
Kristiyan Kostadinov
2017-05-31 02:00:20 +02:00
committed by mmalerba
parent be8d3b23d2
commit 0c03946ac6
2 changed files with 32 additions and 17 deletions
+13
View File
@@ -596,17 +596,21 @@ describe('MdInputContainer', function () {
let fixture: ComponentFixture<MdInputContainerWithFormErrorMessages>;
let testComponent: MdInputContainerWithFormErrorMessages;
let containerEl: HTMLElement;
let inputEl: HTMLElement;
beforeEach(() => {
fixture = TestBed.createComponent(MdInputContainerWithFormErrorMessages);
fixture.detectChanges();
testComponent = fixture.componentInstance;
containerEl = fixture.debugElement.query(By.css('md-input-container')).nativeElement;
inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
});
it('should not show any errors if the user has not interacted', () => {
expect(testComponent.formControl.untouched).toBe(true, 'Expected untouched form control');
expect(containerEl.querySelectorAll('md-error').length).toBe(0, 'Expected no error messages');
expect(inputEl.getAttribute('aria-invalid'))
.toBe('false', 'Expected aria-invalid to be set to "false".');
});
it('should display an error message when the input is touched and invalid', async(() => {
@@ -621,6 +625,8 @@ describe('MdInputContainer', function () {
.toContain('mat-input-invalid', 'Expected container to have the invalid CSS class.');
expect(containerEl.querySelectorAll('md-error').length)
.toBe(1, 'Expected one error message to have been rendered.');
expect(inputEl.getAttribute('aria-invalid'))
.toBe('true', 'Expected aria-invalid to be set to "true".');
});
}));
@@ -638,6 +644,8 @@ describe('MdInputContainer', function () {
.toContain('mat-input-invalid', 'Expected container to have the invalid CSS class.');
expect(containerEl.querySelectorAll('md-error').length)
.toBe(1, 'Expected one error message to have been rendered.');
expect(inputEl.getAttribute('aria-invalid'))
.toBe('true', 'Expected aria-invalid to be set to "true".');
});
}));
@@ -650,9 +658,12 @@ describe('MdInputContainer', function () {
groupFixture.detectChanges();
component = groupFixture.componentInstance;
containerEl = groupFixture.debugElement.query(By.css('md-input-container')).nativeElement;
inputEl = groupFixture.debugElement.query(By.css('input')).nativeElement;
expect(component.formGroup.invalid).toBe(true, 'Expected form control to be invalid');
expect(containerEl.querySelectorAll('md-error').length).toBe(0, 'Expected no error messages');
expect(inputEl.getAttribute('aria-invalid'))
.toBe('false', 'Expected aria-invalid to be set to "false".');
expect(component.formGroupDirective.submitted)
.toBe(false, 'Expected form not to have been submitted');
@@ -666,6 +677,8 @@ describe('MdInputContainer', function () {
.toContain('mat-input-invalid', 'Expected container to have the invalid CSS class.');
expect(containerEl.querySelectorAll('md-error').length)
.toBe(1, 'Expected one error message to have been rendered.');
expect(inputEl.getAttribute('aria-invalid'))
.toBe('true', 'Expected aria-invalid to be set to "true".');
});
}));
+19 -17
View File
@@ -112,6 +112,7 @@ export class MdSuffix {}
'[disabled]': 'disabled',
'[required]': 'required',
'[attr.aria-describedby]': 'ariaDescribedby || null',
'[attr.aria-invalid]': '_isErrorState()',
'(blur)': '_onBlur()',
'(focus)': '_onFocus()',
'(input)': '_onInput()',
@@ -210,7 +211,9 @@ export class MdInputDirective {
constructor(private _elementRef: ElementRef,
private _renderer: Renderer2,
@Optional() @Self() public _ngControl: NgControl) {
@Optional() @Self() public _ngControl: NgControl,
@Optional() private _parentForm: NgForm,
@Optional() private _parentFormGroup: FormGroupDirective) {
// Force setter to be called in case id was not specified.
this.id = this.id;
@@ -233,6 +236,17 @@ export class MdInputDirective {
// FormsModule or ReactiveFormsModule, because Angular forms also listens to input events.
}
/** Whether the input is in an error state. */
_isErrorState(): boolean {
const control = this._ngControl;
const isInvalid = control && control.invalid;
const isTouched = control && control.touched;
const isSubmitted = (this._parentFormGroup && this._parentFormGroup.submitted) ||
(this._parentForm && this._parentForm.submitted);
return !!(isInvalid && (isTouched || isSubmitted));
}
/** Make sure the input is a supported type. */
private _validateType() {
if (MD_INPUT_INVALID_TYPES.indexOf(this._type) !== -1) {
@@ -275,7 +289,7 @@ export class MdInputDirective {
// Remove align attribute to prevent it from interfering with layout.
'[attr.align]': 'null',
'[class.mat-input-container]': 'true',
'[class.mat-input-invalid]': '_isErrorState()',
'[class.mat-input-invalid]': '_mdInputChild._isErrorState()',
'[class.mat-focused]': '_mdInputChild.focused',
'[class.ng-untouched]': '_shouldForward("untouched")',
'[class.ng-touched]': '_shouldForward("touched")',
@@ -354,9 +368,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
constructor(
public _elementRef: ElementRef,
private _changeDetectorRef: ChangeDetectorRef,
@Optional() private _parentForm: NgForm,
@Optional() private _parentFormGroup: FormGroupDirective) { }
private _changeDetectorRef: ChangeDetectorRef) { }
ngAfterContentInit() {
this._validateInputChild();
@@ -390,20 +402,10 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
/** Focuses the underlying input. */
_focusInput() { this._mdInputChild.focus(); }
/** Whether the input container is in an error state. */
_isErrorState(): boolean {
const control = this._mdInputChild._ngControl;
const isInvalid = control && control.invalid;
const isTouched = control && control.touched;
const isSubmitted = (this._parentFormGroup && this._parentFormGroup.submitted) ||
(this._parentForm && this._parentForm.submitted);
return !!(isInvalid && (isTouched || isSubmitted));
}
/** Determines whether to display hints or errors. */
_getDisplayedMessages(): 'error' | 'hint' {
return (this._errorChildren.length > 0 && this._isErrorState()) ? 'error' : 'hint';
let input = this._mdInputChild;
return (this._errorChildren.length > 0 && input._isErrorState()) ? 'error' : 'hint';
}
/**