diff --git a/src/lib/input/input-container.html b/src/lib/input/input-container.html
index 9601f7d86505..50b76067a9c2 100644
--- a/src/lib/input/input-container.html
+++ b/src/lib/input/input-container.html
@@ -30,6 +30,6 @@
[class.md-warn]="dividerColor == 'warn'">
-
{{hintLabel}}
+ {{hintLabel}}
diff --git a/src/lib/input/input-container.spec.ts b/src/lib/input/input-container.spec.ts
index 9ad0cb1ea51a..317a290a1793 100644
--- a/src/lib/input/input-container.spec.ts
+++ b/src/lib/input/input-container.spec.ts
@@ -46,7 +46,9 @@ describe('MdInputContainer', function () {
MdInputContainerWithValueBinding,
MdInputContainerWithFormControl,
MdInputContainerWithStaticPlaceholder,
- MdInputContainerMissingMdInputTestController
+ MdInputContainerMissingMdInputTestController,
+ MdInputContainerMultipleHintTestController,
+ MdInputContainerMultipleHintMixedTestController
],
});
@@ -271,6 +273,17 @@ describe('MdInputContainer', function () {
expect(fixture.debugElement.query(By.css('.md-hint'))).not.toBeNull();
});
+ it('sets an id on hint labels', () => {
+ let fixture = TestBed.createComponent(MdInputContainerHintLabelTestController);
+
+ fixture.componentInstance.label = 'label';
+ fixture.detectChanges();
+
+ let hint = fixture.debugElement.query(By.css('.md-hint')).nativeElement;
+
+ expect(hint.getAttribute('id')).toBeTruthy();
+ });
+
it('supports hint labels elements', () => {
let fixture = TestBed.createComponent(MdInputContainerHintLabel2TestController);
fixture.detectChanges();
@@ -285,6 +298,17 @@ describe('MdInputContainer', function () {
expect(el.textContent).toBe('label');
});
+ it('sets an id on the hint element', () => {
+ let fixture = TestBed.createComponent(MdInputContainerHintLabel2TestController);
+
+ fixture.componentInstance.label = 'label';
+ fixture.detectChanges();
+
+ let hint = fixture.debugElement.query(By.css('md-hint')).nativeElement;
+
+ expect(hint.getAttribute('id')).toBeTruthy();
+ });
+
it('supports placeholder attribute', async(() => {
let fixture = TestBed.createComponent(MdInputContainerPlaceholderAttrTestComponent);
fixture.detectChanges();
@@ -404,6 +428,55 @@ describe('MdInputContainer', function () {
const textarea: HTMLTextAreaElement = fixture.nativeElement.querySelector('textarea');
expect(textarea).not.toBeNull();
});
+
+ it('sets the aria-describedby when a hintLabel is set', () => {
+ let fixture = TestBed.createComponent(MdInputContainerHintLabelTestController);
+
+ fixture.componentInstance.label = 'label';
+ fixture.detectChanges();
+
+ let hint = fixture.debugElement.query(By.css('.md-hint')).nativeElement;
+ let input = fixture.debugElement.query(By.css('input')).nativeElement;
+
+ expect(input.getAttribute('aria-describedby')).toBe(hint.getAttribute('id'));
+ });
+
+ it('sets the aria-describedby to the id of the md-hint', () => {
+ let fixture = TestBed.createComponent(MdInputContainerHintLabel2TestController);
+
+ fixture.componentInstance.label = 'label';
+ fixture.detectChanges();
+
+ let hint = fixture.debugElement.query(By.css('.md-hint')).nativeElement;
+ let input = fixture.debugElement.query(By.css('input')).nativeElement;
+
+ expect(input.getAttribute('aria-describedby')).toBe(hint.getAttribute('id'));
+ });
+
+ it('sets the aria-describedby with multiple md-hint instances', () => {
+ let fixture = TestBed.createComponent(MdInputContainerMultipleHintTestController);
+
+ fixture.componentInstance.startId = 'start';
+ fixture.componentInstance.endId = 'end';
+ fixture.detectChanges();
+
+ let input = fixture.debugElement.query(By.css('input')).nativeElement;
+
+ expect(input.getAttribute('aria-describedby')).toBe('start end');
+ });
+
+ it('sets the aria-describedby when a hintLabel is set, in addition to a md-hint', () => {
+ let fixture = TestBed.createComponent(MdInputContainerMultipleHintMixedTestController);
+
+ fixture.detectChanges();
+
+ let hintLabel = fixture.debugElement.query(By.css('.md-hint')).nativeElement;
+ let endLabel = fixture.debugElement.query(By.css('.md-hint[align="end"]')).nativeElement;
+ let input = fixture.debugElement.query(By.css('input')).nativeElement;
+ let ariaValue = input.getAttribute('aria-describedby');
+
+ expect(ariaValue).toBe(`${hintLabel.getAttribute('id')} ${endLabel.getAttribute('id')}`);
+ });
});
@Component({
@@ -512,6 +585,28 @@ class MdInputContainerInvalidHint2TestController {}
})
class MdInputContainerInvalidHintTestController {}
+@Component({
+ template: `
+
+
+ Hello
+ World
+ `
+})
+class MdInputContainerMultipleHintTestController {
+ startId: string;
+ endId: string;
+}
+
+@Component({
+ template: `
+
+
+ World
+ `
+})
+class MdInputContainerMultipleHintMixedTestController {}
+
@Component({
template: ``
})
diff --git a/src/lib/input/input-container.ts b/src/lib/input/input-container.ts
index 5444848934d4..fabe76933612 100644
--- a/src/lib/input/input-container.ts
+++ b/src/lib/input/input-container.ts
@@ -58,11 +58,15 @@ export class MdPlaceholder {}
host: {
'class': 'md-hint',
'[class.md-right]': 'align == "end"',
+ '[attr.id]': 'id',
}
})
export class MdHint {
// Whether to align the hint label at the start or end of the line.
@Input() align: 'start' | 'end' = 'start';
+
+ // Unique ID for the hint. Used for the aria-describedby on the input.
+ @Input() id: string = `md-input-hint-${nextUniqueId++}`;
}
@@ -77,9 +81,10 @@ export class MdHint {
'[placeholder]': 'placeholder',
'[disabled]': 'disabled',
'[required]': 'required',
+ '[attr.aria-describedby]': 'ariaDescribedby',
'(blur)': '_onBlur()',
'(focus)': '_onFocus()',
- '(input)': '_onInput()'
+ '(input)': '_onInput()',
}
})
export class MdInputDirective {
@@ -95,6 +100,9 @@ export class MdInputDirective {
/** Whether the element is focused or not. */
focused = false;
+ /** Sets the aria-describedby attribute on the input for improved a11y. */
+ ariaDescribedby: string;
+
/** Whether the element is disabled. */
@Input()
get disabled() {
@@ -119,6 +127,7 @@ export class MdInputDirective {
this._placeholderChange.emit(this._placeholder);
}
}
+
/** Whether the element is required. */
@Input()
get required() { return this._required; }
@@ -249,10 +258,13 @@ export class MdInputContainer implements AfterContentInit {
get hintLabel() { return this._hintLabel; }
set hintLabel(value: string) {
this._hintLabel = value;
- this._validateHints();
+ this._processHints();
}
private _hintLabel = '';
+ // Unique id for the hint label.
+ _hintLabelId: string = `md-input-hint-${nextUniqueId++}`;
+
/** Text or the floating placeholder. */
@Input()
get floatingPlaceholder(): boolean { return this._floatingPlaceholder; }
@@ -270,11 +282,11 @@ export class MdInputContainer implements AfterContentInit {
throw new MdInputContainerMissingMdInputError();
}
- this._validateHints();
+ this._processHints();
this._validatePlaceholders();
// Re-validate when things change.
- this._hintChildren.changes.subscribe(() => this._validateHints());
+ this._hintChildren.changes.subscribe(() => this._processHints());
this._mdInputChild._placeholderChange.subscribe(() => this._validatePlaceholders());
}
@@ -287,6 +299,7 @@ export class MdInputContainer implements AfterContentInit {
/** Whether the input has a placeholder. */
_hasPlaceholder() { return !!(this._mdInputChild.placeholder || this._placeholderChild); }
+ /** Focuses the underlying input. */
_focusInput() { this._mdInputChild.focus(); }
/**
@@ -299,6 +312,14 @@ export class MdInputContainer implements AfterContentInit {
}
}
+ /**
+ * Does any extra processing that is required when handling the hints.
+ */
+ private _processHints() {
+ this._validateHints();
+ this._syncAriaDescribedby();
+ }
+
/**
* Ensure that there is a maximum of one of each `` alignment specified, with the
* attribute being considered as `align="start"`.
@@ -322,4 +343,28 @@ export class MdInputContainer implements AfterContentInit {
});
}
}
+
+ /**
+ * Sets the child input's `aria-describedby` to a space-separated list of the ids
+ * of the currently-specified hints, as well as a generated id for the hint label.
+ */
+ private _syncAriaDescribedby() {
+ let ids: string[] = [];
+ let startHint = this._hintChildren ?
+ this._hintChildren.find(hint => hint.align === 'start') : null;
+ let endHint = this._hintChildren ?
+ this._hintChildren.find(hint => hint.align === 'end') : null;
+
+ if (startHint) {
+ ids.push(startHint.id);
+ } else if (this._hintLabel) {
+ ids.push(this._hintLabelId);
+ }
+
+ if (endHint) {
+ ids.push(endHint.id);
+ }
+
+ this._mdInputChild.ariaDescribedby = ids.join(' ');
+ }
}