Skip to content

Commit d25cde7

Browse files
committed
fix(material/form-field): preserve aria-describedby set externally across all form controls
fix(material/form-field): preserve aria-describedby set externally across all form controls add describedbyids and use to preserve existing ids add describedByIds to input add comment Move better sync logic to formfield add describedByIds to other controls update api goldens tweak comment add more tests
1 parent 538b8bc commit d25cde7

File tree

16 files changed

+142
-24
lines changed

16 files changed

+142
-24
lines changed

goldens/material/chips/index.api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ export class MatChipGrid extends MatChipSet implements AfterContentInit, AfterVi
172172
readonly controlType: string;
173173
// (undocumented)
174174
protected _defaultRole: string;
175+
get describedByIds(): string[];
175176
get disabled(): boolean;
176177
set disabled(value: boolean);
177178
get empty(): boolean;
@@ -250,6 +251,7 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy {
250251
// (undocumented)
251252
protected _chipGrid: MatChipGrid;
252253
clear(): void;
254+
get describedByIds(): string[];
253255
get disabled(): boolean;
254256
set disabled(value: boolean);
255257
disabledInteractive: boolean;
@@ -514,6 +516,7 @@ export class MatChipsModule {
514516

515517
// @public
516518
export interface MatChipTextControl {
519+
readonly describedByIds?: string[];
517520
empty: boolean;
518521
focus(): void;
519522
focused: boolean;

goldens/material/datepicker/index.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,7 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
567567
controlType: string;
568568
get dateFilter(): DateFilterFn<D>;
569569
set dateFilter(value: DateFilterFn<D>);
570+
get describedByIds(): string[];
570571
readonly disableAutomaticLabeling = true;
571572
get disabled(): boolean;
572573
set disabled(value: boolean);

goldens/material/form-field/index.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ export type MatFormFieldAppearance = 'fill' | 'outline';
156156
export abstract class MatFormFieldControl<T> {
157157
readonly autofilled?: boolean;
158158
readonly controlType?: string;
159+
readonly describedByIds?: string[];
159160
readonly disableAutomaticLabeling?: boolean;
160161
readonly disabled: boolean;
161162
readonly empty: boolean;

goldens/material/input/index.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ export class MatInput implements MatFormFieldControl_2<any>, OnChanges, OnDestro
152152
constructor(...args: unknown[]);
153153
autofilled: boolean;
154154
controlType: string;
155+
get describedByIds(): string[];
155156
protected _dirtyCheckNativeValue(): void;
156157
get disabled(): boolean;
157158
set disabled(value: BooleanInput);

goldens/material/select/index.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ export class MatSelect implements AfterContentInit, OnChanges, OnDestroy, OnInit
266266
customTrigger: MatSelectTrigger;
267267
// (undocumented)
268268
protected _defaultOptions: MatSelectConfig | null;
269+
get describedByIds(): string[];
269270
protected readonly _destroy: Subject<void>;
270271
readonly disableAutomaticLabeling = true;
271272
disabled: boolean;

src/material/chips/chip-grid.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,14 @@ export class MatChipGrid
349349
this.stateChanges.next();
350350
}
351351

352+
/**
353+
* Implemented as part of MatFormFieldControl.
354+
* @docs-private
355+
*/
356+
get describedByIds(): string[] {
357+
return this._chipInput?.describedByIds || [];
358+
}
359+
352360
/**
353361
* Implemented as part of MatFormFieldControl.
354362
* @docs-private

src/material/chips/chip-input.spec.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,38 @@ describe('MatChipInput', () => {
155155
expect(inputNativeElement.classList).toContain('mat-mdc-chip-input');
156156
expect(inputNativeElement.classList).toContain('mdc-text-field__input');
157157
});
158+
159+
it('should set `aria-describedby` to the id of the mat-hint', () => {
160+
expect(inputNativeElement.getAttribute('aria-describedby')).toBeNull();
161+
162+
fixture.componentInstance.hint = 'test';
163+
fixture.changeDetectorRef.markForCheck();
164+
fixture.detectChanges();
165+
const hint = fixture.debugElement.query(By.css('mat-hint')).nativeElement;
166+
167+
expect(inputNativeElement.getAttribute('aria-describedby')).toBe(hint.getAttribute('id'));
168+
expect(inputNativeElement.getAttribute('aria-describedby')).toMatch(/^mat-mdc-hint-\w+\d+$/);
169+
});
170+
171+
it('should support user binding to `aria-describedby`', () => {
172+
inputNativeElement.setAttribute('aria-describedby', 'test');
173+
fixture.changeDetectorRef.markForCheck();
174+
fixture.detectChanges();
175+
176+
expect(inputNativeElement.getAttribute('aria-describedby')).toBe('test');
177+
});
178+
179+
it('should preserve aria-describedby set directly in the DOM', fakeAsync(() => {
180+
inputNativeElement.setAttribute('aria-describedby', 'custom');
181+
fixture.componentInstance.hint = 'test';
182+
fixture.changeDetectorRef.markForCheck();
183+
fixture.detectChanges();
184+
const hint = fixture.debugElement.query(By.css('mat-hint')).nativeElement;
185+
186+
expect(inputNativeElement.getAttribute('aria-describedby')).toBe(
187+
`${hint.getAttribute('id')} custom`,
188+
);
189+
}));
158190
});
159191

160192
describe('[addOnBlur]', () => {
@@ -289,7 +321,7 @@ describe('MatChipInput', () => {
289321

290322
@Component({
291323
template: `
292-
<mat-form-field>
324+
<mat-form-field [hintLabel]="hint">
293325
<mat-chip-grid #chipGrid [required]="required">
294326
<mat-chip-row>Hello</mat-chip-row>
295327
<input
@@ -309,6 +341,7 @@ class TestChipInput {
309341
placeholder = '';
310342
required = false;
311343
disabledInteractive = false;
344+
hint: string;
312345

313346
add(_: MatChipInputEvent) {}
314347
}

src/material/chips/chip-input.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,17 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy {
218218
this.inputElement.value = '';
219219
}
220220

221+
/**
222+
* Implemented as part of MatChipTextControl.
223+
* @docs-private
224+
*/
225+
get describedByIds(): string[] {
226+
const element = this._elementRef.nativeElement;
227+
const existingDescribedBy = element.getAttribute('aria-describedby');
228+
229+
return existingDescribedBy?.split(' ') || [];
230+
}
231+
221232
setDescribedByIds(ids: string[]): void {
222233
const element = this._elementRef.nativeElement;
223234

src/material/chips/chip-text-control.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ export interface MatChipTextControl {
2323
/** Focuses the text control. */
2424
focus(): void;
2525

26+
/** Gets the list of ids the input is described by. */
27+
readonly describedByIds?: string[];
28+
2629
/** Sets the list of ids the input is described by. */
2730
setDescribedByIds(ids: string[]): void;
2831
}

src/material/datepicker/date-range-input.spec.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ describe('MatDateRangeInput', () => {
169169
expect(rangeInput.getAttribute('aria-labelledby')).toBe(labelId);
170170
});
171171

172-
it('should point the range input aria-labelledby to the form field hint element', () => {
172+
it('should point the range input aria-describedby to the form field hint element', () => {
173173
const fixture = createComponent(StandardRangePicker);
174174
fixture.detectChanges();
175175
const labelId = fixture.nativeElement.querySelector('.mat-mdc-form-field-hint').id;
@@ -179,6 +179,18 @@ describe('MatDateRangeInput', () => {
179179
expect(rangeInput.getAttribute('aria-describedby')).toBe(labelId);
180180
});
181181

182+
it('should preserve aria-describedby set directly in the DOM', fakeAsync(() => {
183+
const fixture = createComponent(StandardRangePicker);
184+
const rangeInput = fixture.nativeElement.querySelector('.mat-date-range-input');
185+
186+
rangeInput.setAttribute('aria-describedby', 'custom');
187+
fixture.changeDetectorRef.markForCheck();
188+
fixture.detectChanges();
189+
const hint = fixture.nativeElement.querySelector('.mat-mdc-form-field-hint');
190+
191+
expect(rangeInput.getAttribute('aria-describedby')).toBe(`${hint.getAttribute('id')} custom`);
192+
}));
193+
182194
it('should not set aria-labelledby if the form field does not have a label', () => {
183195
const fixture = createComponent(RangePickerNoLabel);
184196
fixture.detectChanges();

0 commit comments

Comments
 (0)