mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(input): hints not being read out by screen readers (#2856)
Fixes #2798.
This commit is contained in:
committed by
Kara
parent
d11673adec
commit
f899b5f0b2
@@ -30,6 +30,6 @@
|
||||
[class.md-warn]="dividerColor == 'warn'"></span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="hintLabel != ''" class="md-hint">{{hintLabel}}</div>
|
||||
<div *ngIf="hintLabel != ''" [attr.id]="_hintLabelId" class="md-hint">{{hintLabel}}</div>
|
||||
<ng-content select="md-hint"></ng-content>
|
||||
</div>
|
||||
|
||||
@@ -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: `
|
||||
<md-input-container>
|
||||
<input mdInput>
|
||||
<md-hint align="start" [id]="startId">Hello</md-hint>
|
||||
<md-hint align="end" [id]="endId">World</md-hint>
|
||||
</md-input-container>`
|
||||
})
|
||||
class MdInputContainerMultipleHintTestController {
|
||||
startId: string;
|
||||
endId: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<md-input-container hintLabel="Hello">
|
||||
<input mdInput>
|
||||
<md-hint align="end">World</md-hint>
|
||||
</md-input-container>`
|
||||
})
|
||||
class MdInputContainerMultipleHintMixedTestController {}
|
||||
|
||||
@Component({
|
||||
template: `<md-input-container><input mdInput [(ngModel)]="model"></md-input-container>`
|
||||
})
|
||||
|
||||
@@ -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 `<md-hint>` 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(' ');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user