diff --git a/src/material-experimental/mdc-select/select.spec.ts b/src/material-experimental/mdc-select/select.spec.ts
index 246493586229..f921749377da 100644
--- a/src/material-experimental/mdc-select/select.spec.ts
+++ b/src/material-experimental/mdc-select/select.spec.ts
@@ -222,6 +222,22 @@ describe('MDC-based MatSelect', () => {
expect(select.getAttribute('tabindex')).toEqual('0');
}));
+ it('should set `aria-describedby` to the id of the mat-hint', fakeAsync(() => {
+ expect(select.getAttribute('aria-describedby')).toBeNull();
+
+ fixture.componentInstance.hint = 'test';
+ fixture.detectChanges();
+ const hint = fixture.debugElement.query(By.css('mat-hint')).nativeElement;
+ expect(select.getAttribute('aria-describedby')).toBe(hint.getAttribute('id'));
+ expect(select.getAttribute('aria-describedby')).toMatch(/^mat-mdc-hint-\d+$/);
+ }));
+
+ it('should support user binding to `aria-describedby`', fakeAsync(() => {
+ fixture.componentInstance.ariaDescribedBy = 'test';
+ fixture.detectChanges();
+ expect(select.getAttribute('aria-describedby')).toBe('test');
+ }));
+
it('should be able to override the tabindex', fakeAsync(() => {
fixture.componentInstance.tabIndexOverride = 3;
fixture.detectChanges();
@@ -4223,13 +4239,15 @@ describe('MDC-based MatSelect', () => {
Select a food
{{ food.viewValue }}
+ {{ hint }}
`,
@@ -4250,7 +4268,9 @@ class BasicSelect {
heightAbove = 0;
heightBelow = 0;
hasLabel = true;
+ hint: string;
tabIndexOverride: number;
+ ariaDescribedBy: string;
ariaLabel: string;
ariaLabelledby: string;
panelClass = ['custom-one', 'custom-two'];
diff --git a/src/material-experimental/mdc-select/select.ts b/src/material-experimental/mdc-select/select.ts
index 0c67c796cd05..2256f06c9bee 100644
--- a/src/material-experimental/mdc-select/select.ts
+++ b/src/material-experimental/mdc-select/select.ts
@@ -72,7 +72,6 @@ export class MatSelectTrigger {}
'[attr.aria-required]': 'required.toString()',
'[attr.aria-disabled]': 'disabled.toString()',
'[attr.aria-invalid]': 'errorState',
- '[attr.aria-describedby]': '_ariaDescribedby || null',
'[attr.aria-activedescendant]': '_getAriaActiveDescendant()',
'[class.mat-mdc-select-disabled]': 'disabled',
'[class.mat-mdc-select-invalid]': 'errorState',
diff --git a/src/material/select/select.spec.ts b/src/material/select/select.spec.ts
index 11c25bebadd9..d393c2a9933d 100644
--- a/src/material/select/select.spec.ts
+++ b/src/material/select/select.spec.ts
@@ -298,6 +298,22 @@ describe('MatSelect', () => {
expect(select.getAttribute('aria-labelledby')?.trim()).toBe(valueId);
});
+ it('should set `aria-describedby` to the id of the mat-hint', fakeAsync(() => {
+ expect(select.getAttribute('aria-describedby')).toBeNull();
+
+ fixture.componentInstance.hint = 'test';
+ fixture.detectChanges();
+ const hint = fixture.debugElement.query(By.css('.mat-hint')).nativeElement;
+ expect(select.getAttribute('aria-describedby')).toBe(hint.getAttribute('id'));
+ expect(select.getAttribute('aria-describedby')).toMatch(/^mat-hint-\d+$/);
+ }));
+
+ it('should support user binding to `aria-describedby`', fakeAsync(() => {
+ fixture.componentInstance.ariaDescribedBy = 'test';
+ fixture.detectChanges();
+ expect(select.getAttribute('aria-describedby')).toBe('test');
+ }));
+
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();
@@ -5186,13 +5202,15 @@ describe('MatSelect', () => {
{{ food.viewValue }}
+ {{ hint }}
`,
@@ -5212,7 +5230,9 @@ class BasicSelect {
isRequired: boolean;
heightAbove = 0;
heightBelow = 0;
+ hint: string;
tabIndexOverride: number;
+ ariaDescribedBy: string;
ariaLabel: string;
ariaLabelledby: string;
panelClass = ['custom-one', 'custom-two'];
diff --git a/src/material/select/select.ts b/src/material/select/select.ts
index fd0241d767d6..6ff99d1b019a 100644
--- a/src/material/select/select.ts
+++ b/src/material/select/select.ts
@@ -303,8 +303,11 @@ export abstract class _MatSelectBase
/** Emits whenever the component is destroyed. */
protected readonly _destroy = new Subject();
- /** The aria-describedby attribute on the select for improved a11y. */
- _ariaDescribedby: string;
+ /**
+ * Implemented as part of MatFormFieldControl.
+ * @docs-private
+ */
+ @Input('aria-describedby') userAriaDescribedBy: string;
/** Deals with the selection logic. */
_selectionModel: SelectionModel;
@@ -611,7 +614,7 @@ export abstract class _MatSelectBase
ngOnChanges(changes: SimpleChanges) {
// Updating the disabled state is handled by `mixinDisabled`, but we need to additionally let
// the parent form field know to run change detection when the disabled state changes.
- if (changes['disabled']) {
+ if (changes['disabled'] || changes['userAriaDescribedBy']) {
this.stateChanges.next();
}
@@ -1146,7 +1149,11 @@ export abstract class _MatSelectBase
* @docs-private
*/
setDescribedByIds(ids: string[]) {
- this._ariaDescribedby = ids.join(' ');
+ if (ids.length) {
+ this._elementRef.nativeElement.setAttribute('aria-describedby', ids.join(' '));
+ } else {
+ this._elementRef.nativeElement.removeAttribute('aria-describedby');
+ }
}
/**
@@ -1191,7 +1198,6 @@ export abstract class _MatSelectBase
'[attr.aria-required]': 'required.toString()',
'[attr.aria-disabled]': 'disabled.toString()',
'[attr.aria-invalid]': 'errorState',
- '[attr.aria-describedby]': '_ariaDescribedby || null',
'[attr.aria-activedescendant]': '_getAriaActiveDescendant()',
'[class.mat-select-disabled]': 'disabled',
'[class.mat-select-invalid]': 'errorState',
diff --git a/tools/public_api_guard/material/select.md b/tools/public_api_guard/material/select.md
index a21247cc2fc1..d9c0cea39ae3 100644
--- a/tools/public_api_guard/material/select.md
+++ b/tools/public_api_guard/material/select.md
@@ -114,7 +114,6 @@ export const matSelectAnimations: {
// @public
export abstract class _MatSelectBase extends _MatSelectMixinBase implements AfterContentInit, OnChanges, OnDestroy, OnInit, DoCheck, ControlValueAccessor, CanDisable, HasTabIndex, MatFormFieldControl, CanUpdateErrorState, CanDisableRipple {
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);
- _ariaDescribedby: string;
ariaLabel: string;
ariaLabelledby: string;
protected _canOpen(): boolean;
@@ -203,6 +202,7 @@ export abstract class _MatSelectBase extends _MatSelectMixinBase implements A
get triggerValue(): string;
get typeaheadDebounceInterval(): number;
set typeaheadDebounceInterval(value: NumberInput);
+ userAriaDescribedBy: string;
get value(): any;
set value(newValue: any);
readonly valueChange: EventEmitter;
@@ -211,7 +211,7 @@ export abstract class _MatSelectBase extends _MatSelectMixinBase implements A
protected _viewportRuler: ViewportRuler;
writeValue(value: any): void;
// (undocumented)
- static ɵdir: i0.ɵɵDirectiveDeclaration<_MatSelectBase, 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>;
+ static ɵdir: i0.ɵɵDirectiveDeclaration<_MatSelectBase, 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>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<_MatSelectBase, [null, null, null, null, null, { optional: true; }, { optional: true; }, { optional: true; }, { optional: true; }, { optional: true; self: true; }, { attribute: "tabindex"; }, null, null, { optional: true; }]>;
}