Skip to content

Commit 7c350b1

Browse files
feat(material/select): allow user-defined aria-describedby
1 parent 4cc6b04 commit 7c350b1

File tree

5 files changed

+55
-10
lines changed

5 files changed

+55
-10
lines changed

src/material-experimental/mdc-select/select.spec.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,22 @@ describe('MDC-based MatSelect', () => {
222222
expect(select.getAttribute('tabindex')).toEqual('0');
223223
}));
224224

225+
it('should set `aria-describedby` to the id of the mat-hint', fakeAsync(() => {
226+
expect(select.getAttribute('aria-describedby')).toBeNull();
227+
228+
fixture.componentInstance.hint = 'test';
229+
fixture.detectChanges();
230+
const hint = fixture.debugElement.query(By.css('mat-hint')).nativeElement;
231+
expect(select.getAttribute('aria-describedby')).toBe(hint.getAttribute('id'));
232+
expect(select.getAttribute('aria-describedby')).toMatch(/^mat-mdc-hint-\d+$/);
233+
}));
234+
235+
it('should support user binding to `aria-describedby`', fakeAsync(() => {
236+
fixture.componentInstance.ariaDescribedBy = 'test';
237+
fixture.detectChanges();
238+
expect(select.getAttribute('aria-describedby')).toBe('test');
239+
}));
240+
225241
it('should be able to override the tabindex', fakeAsync(() => {
226242
fixture.componentInstance.tabIndexOverride = 3;
227243
fixture.detectChanges();
@@ -4223,13 +4239,15 @@ describe('MDC-based MatSelect', () => {
42234239
<mat-form-field>
42244240
<mat-label *ngIf="hasLabel">Select a food</mat-label>
42254241
<mat-select placeholder="Food" [formControl]="control" [required]="isRequired"
4226-
[tabIndex]="tabIndexOverride" [aria-label]="ariaLabel" [aria-labelledby]="ariaLabelledby"
4242+
[tabIndex]="tabIndexOverride" [aria-describedby]="ariaDescribedBy"
4243+
[aria-label]="ariaLabel" [aria-labelledby]="ariaLabelledby"
42274244
[panelClass]="panelClass" [disableRipple]="disableRipple"
42284245
[typeaheadDebounceInterval]="typeaheadDebounceInterval">
42294246
<mat-option *ngFor="let food of foods" [value]="food.value" [disabled]="food.disabled">
42304247
{{ food.viewValue }}
42314248
</mat-option>
42324249
</mat-select>
4250+
<mat-hint *ngIf="hint">{{ hint }}</mat-hint>
42334251
</mat-form-field>
42344252
<div [style.height.px]="heightBelow"></div>
42354253
`,
@@ -4250,7 +4268,9 @@ class BasicSelect {
42504268
heightAbove = 0;
42514269
heightBelow = 0;
42524270
hasLabel = true;
4271+
hint: string;
42534272
tabIndexOverride: number;
4273+
ariaDescribedBy: string;
42544274
ariaLabel: string;
42554275
ariaLabelledby: string;
42564276
panelClass = ['custom-one', 'custom-two'];

src/material-experimental/mdc-select/select.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ export class MatSelectTrigger {}
7272
'[attr.aria-required]': 'required.toString()',
7373
'[attr.aria-disabled]': 'disabled.toString()',
7474
'[attr.aria-invalid]': 'errorState',
75-
'[attr.aria-describedby]': '_ariaDescribedby || null',
7675
'[attr.aria-activedescendant]': '_getAriaActiveDescendant()',
7776
'[class.mat-mdc-select-disabled]': 'disabled',
7877
'[class.mat-mdc-select-invalid]': 'errorState',

src/material/select/select.spec.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,22 @@ describe('MatSelect', () => {
298298
expect(select.getAttribute('aria-labelledby')?.trim()).toBe(valueId);
299299
});
300300

301+
it('should set `aria-describedby` to the id of the mat-hint', fakeAsync(() => {
302+
expect(select.getAttribute('aria-describedby')).toBeNull();
303+
304+
fixture.componentInstance.hint = 'test';
305+
fixture.detectChanges();
306+
const hint = fixture.debugElement.query(By.css('.mat-hint')).nativeElement;
307+
expect(select.getAttribute('aria-describedby')).toBe(hint.getAttribute('id'));
308+
expect(select.getAttribute('aria-describedby')).toMatch(/^mat-hint-\d+$/);
309+
}));
310+
311+
it('should support user binding to `aria-describedby`', fakeAsync(() => {
312+
fixture.componentInstance.ariaDescribedBy = 'test';
313+
fixture.detectChanges();
314+
expect(select.getAttribute('aria-describedby')).toBe('test');
315+
}));
316+
301317
it('should select options via the UP/DOWN arrow keys on a closed select', fakeAsync(() => {
302318
const formControl = fixture.componentInstance.control;
303319
const options = fixture.componentInstance.options.toArray();
@@ -5186,13 +5202,15 @@ describe('MatSelect', () => {
51865202
<div [style.height.px]="heightAbove"></div>
51875203
<mat-form-field>
51885204
<mat-select placeholder="Food" [formControl]="control" [required]="isRequired"
5189-
[tabIndex]="tabIndexOverride" [aria-label]="ariaLabel" [aria-labelledby]="ariaLabelledby"
5205+
[tabIndex]="tabIndexOverride" [aria-describedby]="ariaDescribedBy"
5206+
[aria-label]="ariaLabel" [aria-labelledby]="ariaLabelledby"
51905207
[panelClass]="panelClass" [disableRipple]="disableRipple"
51915208
[typeaheadDebounceInterval]="typeaheadDebounceInterval">
51925209
<mat-option *ngFor="let food of foods" [value]="food.value" [disabled]="food.disabled">
51935210
{{ food.viewValue }}
51945211
</mat-option>
51955212
</mat-select>
5213+
<mat-hint *ngIf="hint">{{ hint }}</mat-hint>
51965214
</mat-form-field>
51975215
<div [style.height.px]="heightBelow"></div>
51985216
`,
@@ -5212,7 +5230,9 @@ class BasicSelect {
52125230
isRequired: boolean;
52135231
heightAbove = 0;
52145232
heightBelow = 0;
5233+
hint: string;
52155234
tabIndexOverride: number;
5235+
ariaDescribedBy: string;
52165236
ariaLabel: string;
52175237
ariaLabelledby: string;
52185238
panelClass = ['custom-one', 'custom-two'];

src/material/select/select.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,11 @@ export abstract class _MatSelectBase<C>
303303
/** Emits whenever the component is destroyed. */
304304
protected readonly _destroy = new Subject<void>();
305305

306-
/** The aria-describedby attribute on the select for improved a11y. */
307-
_ariaDescribedby: string;
306+
/**
307+
* Implemented as part of MatFormFieldControl.
308+
* @docs-private
309+
*/
310+
@Input('aria-describedby') userAriaDescribedBy: string;
308311

309312
/** Deals with the selection logic. */
310313
_selectionModel: SelectionModel<MatOption>;
@@ -611,7 +614,7 @@ export abstract class _MatSelectBase<C>
611614
ngOnChanges(changes: SimpleChanges) {
612615
// Updating the disabled state is handled by `mixinDisabled`, but we need to additionally let
613616
// the parent form field know to run change detection when the disabled state changes.
614-
if (changes['disabled']) {
617+
if (changes['disabled'] || changes['userAriaDescribedBy']) {
615618
this.stateChanges.next();
616619
}
617620

@@ -1146,7 +1149,11 @@ export abstract class _MatSelectBase<C>
11461149
* @docs-private
11471150
*/
11481151
setDescribedByIds(ids: string[]) {
1149-
this._ariaDescribedby = ids.join(' ');
1152+
if (ids.length) {
1153+
this._elementRef.nativeElement.setAttribute('aria-describedby', ids.join(' '));
1154+
} else {
1155+
this._elementRef.nativeElement.removeAttribute('aria-describedby');
1156+
}
11501157
}
11511158

11521159
/**
@@ -1191,7 +1198,6 @@ export abstract class _MatSelectBase<C>
11911198
'[attr.aria-required]': 'required.toString()',
11921199
'[attr.aria-disabled]': 'disabled.toString()',
11931200
'[attr.aria-invalid]': 'errorState',
1194-
'[attr.aria-describedby]': '_ariaDescribedby || null',
11951201
'[attr.aria-activedescendant]': '_getAriaActiveDescendant()',
11961202
'[class.mat-select-disabled]': 'disabled',
11971203
'[class.mat-select-invalid]': 'errorState',

tools/public_api_guard/material/select.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ export const matSelectAnimations: {
114114
// @public
115115
export abstract class _MatSelectBase<C> extends _MatSelectMixinBase implements AfterContentInit, OnChanges, OnDestroy, OnInit, DoCheck, ControlValueAccessor, CanDisable, HasTabIndex, MatFormFieldControl<any>, CanUpdateErrorState, CanDisableRipple {
116116
constructor(_viewportRuler: ViewportRuler, _changeDetectorRef: ChangeDetectorRef, _ngZone: NgZone, _defaultErrorStateMatcher: ErrorStateMatcher, elementRef: ElementRef, _dir: Directionality, _parentForm: NgForm, _parentFormGroup: FormGroupDirective, _parentFormField: MatFormField, ngControl: NgControl, tabIndex: string, scrollStrategyFactory: any, _liveAnnouncer: LiveAnnouncer, _defaultOptions?: MatSelectConfig | undefined);
117-
_ariaDescribedby: string;
118117
ariaLabel: string;
119118
ariaLabelledby: string;
120119
protected _canOpen(): boolean;
@@ -203,6 +202,7 @@ export abstract class _MatSelectBase<C> extends _MatSelectMixinBase implements A
203202
get triggerValue(): string;
204203
get typeaheadDebounceInterval(): number;
205204
set typeaheadDebounceInterval(value: NumberInput);
205+
userAriaDescribedBy: string;
206206
get value(): any;
207207
set value(newValue: any);
208208
readonly valueChange: EventEmitter<any>;
@@ -211,7 +211,7 @@ export abstract class _MatSelectBase<C> extends _MatSelectMixinBase implements A
211211
protected _viewportRuler: ViewportRuler;
212212
writeValue(value: any): void;
213213
// (undocumented)
214-
static ɵdir: i0.ɵɵDirectiveDeclaration<_MatSelectBase<any>, never, never, { "panelClass": "panelClass"; "placeholder": "placeholder"; "required": "required"; "multiple": "multiple"; "disableOptionCentering": "disableOptionCentering"; "compareWith": "compareWith"; "value": "value"; "ariaLabel": "aria-label"; "ariaLabelledby": "aria-labelledby"; "errorStateMatcher": "errorStateMatcher"; "typeaheadDebounceInterval": "typeaheadDebounceInterval"; "sortComparator": "sortComparator"; "id": "id"; }, { "openedChange": "openedChange"; "_openedStream": "opened"; "_closedStream": "closed"; "selectionChange": "selectionChange"; "valueChange": "valueChange"; }, never>;
214+
static ɵdir: i0.ɵɵDirectiveDeclaration<_MatSelectBase<any>, never, never, { "userAriaDescribedBy": "aria-describedby"; "panelClass": "panelClass"; "placeholder": "placeholder"; "required": "required"; "multiple": "multiple"; "disableOptionCentering": "disableOptionCentering"; "compareWith": "compareWith"; "value": "value"; "ariaLabel": "aria-label"; "ariaLabelledby": "aria-labelledby"; "errorStateMatcher": "errorStateMatcher"; "typeaheadDebounceInterval": "typeaheadDebounceInterval"; "sortComparator": "sortComparator"; "id": "id"; }, { "openedChange": "openedChange"; "_openedStream": "opened"; "_closedStream": "closed"; "selectionChange": "selectionChange"; "valueChange": "valueChange"; }, never>;
215215
// (undocumented)
216216
static ɵfac: i0.ɵɵFactoryDeclaration<_MatSelectBase<any>, [null, null, null, null, null, { optional: true; }, { optional: true; }, { optional: true; }, { optional: true; }, { optional: true; self: true; }, { attribute: "tabindex"; }, null, null, { optional: true; }]>;
217217
}

0 commit comments

Comments
 (0)