mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(select): label not being read out when using mat-label in mat-form-field (#11710)
Fixes the `mat-select` label not being read out by screen readers, if it's specified via a `mat-label` on a `mat-form-field`.
This commit is contained in:
committed by
Jeremy Elbourn
parent
69629adeab
commit
e349fe4d7c
@@ -12,6 +12,7 @@
|
||||
<!-- We add aria-owns as a workaround for an issue in JAWS & NVDA where the label isn't
|
||||
read if it comes before the control in the DOM. -->
|
||||
<label class="mat-form-field-label"
|
||||
[id]="_labelId"
|
||||
[attr.for]="_control.id"
|
||||
[attr.aria-owns]="_control.id"
|
||||
[class.mat-empty]="_control.empty && !_shouldAlwaysFloat"
|
||||
|
||||
@@ -186,6 +186,9 @@ export class MatFormField extends _MatFormFieldMixinBase
|
||||
// Unique id for the hint label.
|
||||
_hintLabelId: string = `mat-hint-${nextUniqueId++}`;
|
||||
|
||||
// Unique id for the internal form field label.
|
||||
_labelId = `mat-form-field-label-${nextUniqueId++}`;
|
||||
|
||||
/**
|
||||
* Whether the label should always float, never float or float as the user types.
|
||||
*
|
||||
|
||||
@@ -124,6 +124,7 @@ describe('MatSelect', () => {
|
||||
MultiSelect,
|
||||
SelectWithGroups,
|
||||
SelectWithGroupsAndNgContainer,
|
||||
SelectWithFormFieldLabel,
|
||||
]);
|
||||
}));
|
||||
|
||||
@@ -230,6 +231,29 @@ describe('MatSelect', () => {
|
||||
expect(select.getAttribute('tabindex')).toEqual('0');
|
||||
}));
|
||||
|
||||
it('should set `aria-labelledby` to form field label if there is no placeholder', () => {
|
||||
fixture.destroy();
|
||||
|
||||
const labelFixture = TestBed.createComponent(SelectWithFormFieldLabel);
|
||||
labelFixture.detectChanges();
|
||||
select = labelFixture.debugElement.query(By.css('mat-select')).nativeElement;
|
||||
|
||||
expect(select.getAttribute('aria-labelledby')).toBeTruthy();
|
||||
expect(select.getAttribute('aria-labelledby'))
|
||||
.toBe(labelFixture.nativeElement.querySelector('label').getAttribute('id'));
|
||||
});
|
||||
|
||||
it('should not set `aria-labelledby` if there is a placeholder', () => {
|
||||
fixture.destroy();
|
||||
|
||||
const labelFixture = TestBed.createComponent(SelectWithFormFieldLabel);
|
||||
labelFixture.componentInstance.placeholder = 'Thing selector';
|
||||
labelFixture.detectChanges();
|
||||
select = labelFixture.debugElement.query(By.css('mat-select')).nativeElement;
|
||||
|
||||
expect(select.getAttribute('aria-labelledby')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should select options via the UP/DOWN arrow keys on a closed select', fakeAsync(() => {
|
||||
const formControl = fixture.componentInstance.control;
|
||||
const options = fixture.componentInstance.options.toArray();
|
||||
@@ -4561,3 +4585,18 @@ class SelectWithoutOptionCentering {
|
||||
@ViewChild(MatSelect) select: MatSelect;
|
||||
@ViewChildren(MatOption) options: QueryList<MatOption>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<mat-form-field>
|
||||
<mat-label>Select a thing</mat-label>
|
||||
|
||||
<mat-select [placeholder]="placeholder">
|
||||
<mat-option value="thing">A thing</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
`
|
||||
})
|
||||
class SelectWithFormFieldLabel {
|
||||
placeholder: string;
|
||||
}
|
||||
|
||||
@@ -189,8 +189,8 @@ export class MatSelectTrigger {}
|
||||
'role': 'listbox',
|
||||
'[attr.id]': 'id',
|
||||
'[attr.tabindex]': 'tabIndex',
|
||||
'[attr.aria-label]': '_ariaLabel',
|
||||
'[attr.aria-labelledby]': 'ariaLabelledby',
|
||||
'[attr.aria-label]': '_getAriaLabel()',
|
||||
'[attr.aria-labelledby]': '_getAriaLabelledby()',
|
||||
'[attr.aria-required]': 'required.toString()',
|
||||
'[attr.aria-disabled]': 'disabled.toString()',
|
||||
'[attr.aria-invalid]': 'errorState',
|
||||
@@ -1017,12 +1017,27 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
|
||||
}
|
||||
|
||||
/** Returns the aria-label of the select component. */
|
||||
get _ariaLabel(): string | null {
|
||||
// If an ariaLabelledby value has been set, the select should not overwrite the
|
||||
_getAriaLabel(): string | null {
|
||||
// If an ariaLabelledby value has been set by the consumer, the select should not overwrite the
|
||||
// `aria-labelledby` value by setting the ariaLabel to the placeholder.
|
||||
return this.ariaLabelledby ? null : this.ariaLabel || this.placeholder;
|
||||
}
|
||||
|
||||
/** Returns the aria-labelledby of the select component. */
|
||||
_getAriaLabelledby(): string | null {
|
||||
if (this.ariaLabelledby) {
|
||||
return this.ariaLabelledby;
|
||||
}
|
||||
|
||||
// Note: we use `_getAriaLabel` here, because we want to check whether there's a
|
||||
// computed label. `this.ariaLabel` is only the user-specified label.
|
||||
if (!this._parentFormField || this._getAriaLabel()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._parentFormField._labelId || null;
|
||||
}
|
||||
|
||||
/** Determines the `aria-activedescendant` to be set on the host. */
|
||||
_getAriaActiveDescendant(): string | null {
|
||||
if (this.panelOpen && this._keyManager && this._keyManager.activeItem) {
|
||||
|
||||
Reference in New Issue
Block a user