Skip to content

Commit 2af284c

Browse files
mmalerbatinayuangao
authored andcommitted
fix(input): update aria-describedby to also include errors (#6239)
* fix(input): update aria-describedby to also include errors * comments addressed * check for null errorChildren
1 parent 73c6d8d commit 2af284c

File tree

2 files changed

+56
-20
lines changed

2 files changed

+56
-20
lines changed

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,24 @@ describe('MdInputContainer with forms', () => {
813813
.toBe(1, 'Expected one hint to still be shown.');
814814
});
815815
}));
816+
817+
it('sets the aria-describedby to reference errors when in error state', () => {
818+
let hintId = fixture.debugElement.query(By.css('.mat-hint')).nativeElement.getAttribute('id');
819+
let describedBy = inputEl.getAttribute('aria-describedby');
820+
821+
expect(hintId).toBeTruthy('hint should be shown');
822+
expect(describedBy).toBe(hintId);
823+
824+
fixture.componentInstance.formControl.markAsTouched();
825+
fixture.detectChanges();
826+
827+
let errorIds = fixture.debugElement.queryAll(By.css('.mat-input-error'))
828+
.map(el => el.nativeElement.getAttribute('id')).join(' ');
829+
describedBy = inputEl.getAttribute('aria-describedby');
830+
831+
expect(errorIds).toBeTruthy('errors should be shown');
832+
expect(describedBy).toBe(errorIds);
833+
});
816834
});
817835

818836
describe('custom error behavior', () => {

src/lib/input/input-container.ts

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
MD_ERROR_GLOBAL_OPTIONS
5252
} from '../core/error/error-options';
5353
import {Subject} from 'rxjs/Subject';
54+
import {startWith} from '@angular/cdk/rxjs';
5455

5556
// Invalid input type. Using one of these will throw an MdInputContainerUnsupportedTypeError.
5657
const MD_INPUT_INVALID_TYPES = [
@@ -100,10 +101,13 @@ export class MdHint {
100101
@Directive({
101102
selector: 'md-error, mat-error',
102103
host: {
103-
'class': 'mat-input-error'
104+
'class': 'mat-input-error',
105+
'[attr.id]': 'id',
104106
}
105107
})
106-
export class MdErrorDirective { }
108+
export class MdErrorDirective {
109+
@Input() id: string = `md-input-error-${nextUniqueId++}`;
110+
}
107111

108112
/** Prefix to be placed the the front of the input. */
109113
@Directive({
@@ -474,12 +478,11 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
474478

475479
ngAfterContentInit() {
476480
this._validateInputChild();
477-
this._processHints();
478-
this._validatePlaceholders();
479481

480482
// Subscribe to changes in the child input state in order to update the container UI.
481-
this._mdInputChild._stateChanges.subscribe(() => {
483+
startWith.call(this._mdInputChild._stateChanges, null).subscribe(() => {
482484
this._validatePlaceholders();
485+
this._syncAriaDescribedby();
483486
this._changeDetectorRef.markForCheck();
484487
});
485488

@@ -489,8 +492,17 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
489492
});
490493
}
491494

492-
// Re-validate when the amount of hints changes.
493-
this._hintChildren.changes.subscribe(() => this._processHints());
495+
// Re-validate when the number of hints changes.
496+
startWith.call(this._hintChildren.changes, null).subscribe(() => {
497+
this._processHints();
498+
this._changeDetectorRef.markForCheck();
499+
});
500+
501+
// Update the aria-described by when the number of errors changes.
502+
startWith.call(this._errorChildren.changes, null).subscribe(() => {
503+
this._syncAriaDescribedby();
504+
this._changeDetectorRef.markForCheck();
505+
});
494506
}
495507

496508
ngAfterContentChecked() {
@@ -522,7 +534,8 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
522534
/** Determines whether to display hints or errors. */
523535
_getDisplayedMessages(): 'error' | 'hint' {
524536
let input = this._mdInputChild;
525-
return (this._errorChildren.length > 0 && input._isErrorState) ? 'error' : 'hint';
537+
return (this._errorChildren && this._errorChildren.length > 0 && input._isErrorState) ?
538+
'error' : 'hint';
526539
}
527540

528541
/**
@@ -574,19 +587,24 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
574587
private _syncAriaDescribedby() {
575588
if (this._mdInputChild) {
576589
let ids: string[] = [];
577-
let startHint = this._hintChildren ?
578-
this._hintChildren.find(hint => hint.align === 'start') : null;
579-
let endHint = this._hintChildren ?
580-
this._hintChildren.find(hint => hint.align === 'end') : null;
581-
582-
if (startHint) {
583-
ids.push(startHint.id);
584-
} else if (this._hintLabel) {
585-
ids.push(this._hintLabelId);
586-
}
587590

588-
if (endHint) {
589-
ids.push(endHint.id);
591+
if (this._getDisplayedMessages() === 'hint') {
592+
let startHint = this._hintChildren ?
593+
this._hintChildren.find(hint => hint.align === 'start') : null;
594+
let endHint = this._hintChildren ?
595+
this._hintChildren.find(hint => hint.align === 'end') : null;
596+
597+
if (startHint) {
598+
ids.push(startHint.id);
599+
} else if (this._hintLabel) {
600+
ids.push(this._hintLabelId);
601+
}
602+
603+
if (endHint) {
604+
ids.push(endHint.id);
605+
}
606+
} else if (this._errorChildren) {
607+
ids = this._errorChildren.map(mdError => mdError.id);
590608
}
591609

592610
this._mdInputChild.ariaDescribedby = ids.join(' ');

0 commit comments

Comments
 (0)