Skip to content

Commit a5aade2

Browse files
authored
fix(material/form-field): preserve aria-describedby set externally across all form controls (angular#30699)
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 63e3c4a commit a5aade2

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
@@ -566,6 +566,7 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
566566
controlType: string;
567567
get dateFilter(): DateFilterFn<D>;
568568
set dateFilter(value: DateFilterFn<D>);
569+
get describedByIds(): string[];
569570
readonly disableAutomaticLabeling = true;
570571
get disabled(): boolean;
571572
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
@@ -156,6 +156,38 @@ describe('MatChipInput', () => {
156156
expect(inputNativeElement.classList).toContain('mat-mdc-chip-input');
157157
expect(inputNativeElement.classList).toContain('mdc-text-field__input');
158158
});
159+
160+
it('should set `aria-describedby` to the id of the mat-hint', () => {
161+
expect(inputNativeElement.getAttribute('aria-describedby')).toBeNull();
162+
163+
fixture.componentInstance.hint = 'test';
164+
fixture.changeDetectorRef.markForCheck();
165+
fixture.detectChanges();
166+
const hint = fixture.debugElement.query(By.css('mat-hint')).nativeElement;
167+
168+
expect(inputNativeElement.getAttribute('aria-describedby')).toBe(hint.getAttribute('id'));
169+
expect(inputNativeElement.getAttribute('aria-describedby')).toMatch(/^mat-mdc-hint-\w+\d+$/);
170+
});
171+
172+
it('should support user binding to `aria-describedby`', () => {
173+
inputNativeElement.setAttribute('aria-describedby', 'test');
174+
fixture.changeDetectorRef.markForCheck();
175+
fixture.detectChanges();
176+
177+
expect(inputNativeElement.getAttribute('aria-describedby')).toBe('test');
178+
});
179+
180+
it('should preserve aria-describedby set directly in the DOM', fakeAsync(() => {
181+
inputNativeElement.setAttribute('aria-describedby', 'custom');
182+
fixture.componentInstance.hint = 'test';
183+
fixture.changeDetectorRef.markForCheck();
184+
fixture.detectChanges();
185+
const hint = fixture.debugElement.query(By.css('mat-hint')).nativeElement;
186+
187+
expect(inputNativeElement.getAttribute('aria-describedby')).toBe(
188+
`${hint.getAttribute('id')} custom`,
189+
);
190+
}));
159191
});
160192

161193
describe('[addOnBlur]', () => {
@@ -291,7 +323,7 @@ describe('MatChipInput', () => {
291323

292324
@Component({
293325
template: `
294-
<mat-form-field>
326+
<mat-form-field [hintLabel]="hint">
295327
<mat-chip-grid #chipGrid [required]="required">
296328
<mat-chip-row>Hello</mat-chip-row>
297329
<input
@@ -311,6 +343,7 @@ class TestChipInput {
311343
placeholder = '';
312344
required = false;
313345
disabledInteractive = false;
346+
hint: string;
314347

315348
add(_: MatChipInputEvent) {}
316349
}

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
@@ -170,7 +170,7 @@ describe('MatDateRangeInput', () => {
170170
expect(rangeInput.getAttribute('aria-labelledby')).toBe(labelId);
171171
});
172172

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

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

0 commit comments

Comments
 (0)