Skip to content

Commit e1a1b4f

Browse files
fix(material-experimental/mdc-chips): Mirror aria-describedby to matChipInput
Updates mat-chip-grid to associate any ids set for aria-describedby to the matChipInput instance within the grid, if one exists. Removes the aria-describedby attribute on the grid itself since it never receives focus. Fixes #24542
1 parent 73dde84 commit e1a1b4f

File tree

6 files changed

+96
-44
lines changed

6 files changed

+96
-44
lines changed

src/material-experimental/mdc-chips/chip-grid.spec.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -853,13 +853,18 @@ describe('MDC-based MatChipGrid', () => {
853853
let errorTestComponent: ChipGridWithFormErrorMessages;
854854
let containerEl: HTMLElement;
855855
let chipGridEl: HTMLElement;
856+
let inputEl: HTMLElement;
856857

857-
beforeEach(() => {
858+
beforeEach(fakeAsync(() => {
858859
fixture = createComponent(ChipGridWithFormErrorMessages);
860+
flush();
861+
fixture.detectChanges();
862+
859863
errorTestComponent = fixture.componentInstance;
860864
containerEl = fixture.debugElement.query(By.css('mat-form-field'))!.nativeElement;
861865
chipGridEl = fixture.debugElement.query(By.css('mat-chip-grid'))!.nativeElement;
862-
});
866+
inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
867+
}));
863868

864869
it('should not show any errors if the user has not interacted', () => {
865870
expect(errorTestComponent.formControl.untouched)
@@ -908,6 +913,7 @@ describe('MDC-based MatChipGrid', () => {
908913
.toBe(0);
909914

910915
dispatchFakeEvent(fixture.debugElement.query(By.css('form'))!.nativeElement, 'submit');
916+
flush();
911917
fixture.detectChanges();
912918

913919
fixture.whenStable().then(() => {
@@ -924,10 +930,12 @@ describe('MDC-based MatChipGrid', () => {
924930
.withContext('Expected aria-invalid to be set to "true".')
925931
.toBe('true');
926932
});
933+
flush();
927934
}));
928935

929936
it('should hide the errors and show the hints once the chip grid becomes valid', fakeAsync(() => {
930937
errorTestComponent.formControl.markAsTouched();
938+
flush();
931939
fixture.detectChanges();
932940

933941
fixture.whenStable().then(() => {
@@ -942,6 +950,7 @@ describe('MDC-based MatChipGrid', () => {
942950
.toBe(0);
943951

944952
errorTestComponent.formControl.setValue('something');
953+
flush();
945954
fixture.detectChanges();
946955

947956
fixture.whenStable().then(() => {
@@ -956,6 +965,8 @@ describe('MDC-based MatChipGrid', () => {
956965
.withContext('Expected one hint to be shown once the input is valid.')
957966
.toBe(1);
958967
});
968+
969+
flush();
959970
});
960971
}));
961972

@@ -966,27 +977,31 @@ describe('MDC-based MatChipGrid', () => {
966977
expect(containerEl.querySelector('mat-error')!.getAttribute('aria-live')).toBe('polite');
967978
});
968979

969-
it('sets the aria-describedby to reference errors when in error state', () => {
980+
it('sets the aria-describedby on the input to reference errors when in error state', fakeAsync(() => {
970981
let hintId = fixture.debugElement
971982
.query(By.css('.mat-mdc-form-field-hint'))!
972983
.nativeElement.getAttribute('id');
973-
let describedBy = chipGridEl.getAttribute('aria-describedby');
984+
let describedBy = inputEl.getAttribute('aria-describedby');
974985

975986
expect(hintId).withContext('hint should be shown').toBeTruthy();
976987
expect(describedBy).toBe(hintId);
977988

978989
fixture.componentInstance.formControl.markAsTouched();
979990
fixture.detectChanges();
980991

992+
// Flush the describedby timer and detect changes caused by it.
993+
flush();
994+
fixture.detectChanges();
995+
981996
let errorIds = fixture.debugElement
982997
.queryAll(By.css('.mat-mdc-form-field-error'))
983998
.map(el => el.nativeElement.getAttribute('id'))
984999
.join(' ');
985-
describedBy = chipGridEl.getAttribute('aria-describedby');
1000+
let errorDescribedBy = inputEl.getAttribute('aria-describedby');
9861001

9871002
expect(errorIds).withContext('errors should be shown').toBeTruthy();
988-
expect(describedBy).toBe(errorIds);
989-
});
1003+
expect(errorDescribedBy).toBe(errorIds);
1004+
}));
9901005
});
9911006

9921007
function createComponent<T>(

src/material-experimental/mdc-chips/chip-grid.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,6 @@ const _MatChipGridMixinBase = mixinErrorState(MatChipGridBase);
108108
'class': 'mat-mdc-chip-set mat-mdc-chip-grid mdc-evolution-chip-set',
109109
'[attr.role]': 'role',
110110
'[tabIndex]': '_chips && _chips.length === 0 ? -1 : tabIndex',
111-
// TODO: replace this binding with use of AriaDescriber
112-
'[attr.aria-describedby]': '_ariaDescribedby || null',
113111
'[attr.aria-disabled]': 'disabled.toString()',
114112
'[attr.aria-invalid]': 'errorState',
115113
'[class.mat-mdc-chip-list-disabled]': 'disabled',
@@ -145,6 +143,11 @@ export class MatChipGrid
145143

146144
protected override _defaultRole = 'grid';
147145

146+
/**
147+
* List of element ids to propagate to the chipInput's aria-describedby attribute.
148+
*/
149+
private _ariaDescribedbyIds: string[] = [];
150+
148151
/**
149152
* Function when touched. Set as part of ControlValueAccessor implementation.
150153
* @docs-private
@@ -337,6 +340,7 @@ export class MatChipGrid
337340
/** Associates an HTML input element with this chip grid. */
338341
registerInput(inputElement: MatChipTextControl): void {
339342
this._chipInput = inputElement;
343+
this._chipInput.setDescribedByIds(this._ariaDescribedbyIds);
340344
}
341345

342346
/**
@@ -378,7 +382,18 @@ export class MatChipGrid
378382
* @docs-private
379383
*/
380384
setDescribedByIds(ids: string[]) {
381-
this._ariaDescribedby = ids.join(' ');
385+
// We must keep this up to date to handle the case where ids are set
386+
// before the chip input is registered.
387+
this._ariaDescribedbyIds = ids;
388+
389+
if (this._chipInput) {
390+
// Use a setTimeout in case this is being run during change detection
391+
// and the chip input has already determined its host binding for
392+
// aria-describedBy.
393+
setTimeout(() => {
394+
this._chipInput.setDescribedByIds(ids);
395+
}, 0);
396+
}
382397
}
383398

384399
/**

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

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,39 +25,35 @@ describe('MDC-based MatChipInput', () => {
2525
let chipInputDirective: MatChipInput;
2626
let dir = 'ltr';
2727

28-
beforeEach(
29-
waitForAsync(() => {
30-
TestBed.configureTestingModule({
31-
imports: [PlatformModule, MatChipsModule, MatFormFieldModule, NoopAnimationsModule],
32-
declarations: [TestChipInput],
33-
providers: [
34-
{
35-
provide: Directionality,
36-
useFactory: () => {
37-
return {
38-
value: dir.toLowerCase(),
39-
change: new Subject(),
40-
};
41-
},
28+
beforeEach(waitForAsync(() => {
29+
TestBed.configureTestingModule({
30+
imports: [PlatformModule, MatChipsModule, MatFormFieldModule, NoopAnimationsModule],
31+
declarations: [TestChipInput],
32+
providers: [
33+
{
34+
provide: Directionality,
35+
useFactory: () => {
36+
return {
37+
value: dir.toLowerCase(),
38+
change: new Subject(),
39+
};
4240
},
43-
],
44-
});
41+
},
42+
],
43+
});
4544

46-
TestBed.compileComponents();
47-
}),
48-
);
45+
TestBed.compileComponents();
46+
}));
4947

50-
beforeEach(
51-
waitForAsync(() => {
52-
fixture = TestBed.createComponent(TestChipInput);
53-
testChipInput = fixture.debugElement.componentInstance;
54-
fixture.detectChanges();
48+
beforeEach(waitForAsync(() => {
49+
fixture = TestBed.createComponent(TestChipInput);
50+
testChipInput = fixture.debugElement.componentInstance;
51+
fixture.detectChanges();
5552

56-
inputDebugElement = fixture.debugElement.query(By.directive(MatChipInput))!;
57-
chipInputDirective = inputDebugElement.injector.get<MatChipInput>(MatChipInput);
58-
inputNativeElement = inputDebugElement.nativeElement;
59-
}),
60-
);
53+
inputDebugElement = fixture.debugElement.query(By.directive(MatChipInput))!;
54+
chipInputDirective = inputDebugElement.injector.get<MatChipInput>(MatChipInput);
55+
inputNativeElement = inputDebugElement.nativeElement;
56+
}));
6157

6258
describe('basic behavior', () => {
6359
it('emits the (chipEnd) on enter keyup', () => {
@@ -230,6 +226,26 @@ describe('MDC-based MatChipInput', () => {
230226
dispatchKeyboardEvent(inputNativeElement, 'keydown', ENTER, undefined, {shift: true});
231227
expect(testChipInput.add).not.toHaveBeenCalled();
232228
});
229+
230+
it('should set aria-describedby correctly when a non-empty list of ids is passed to setDescribedByIds', fakeAsync(() => {
231+
const ids = ['a', 'b', 'c'];
232+
233+
testChipInput.chipGridInstance.setDescribedByIds(ids);
234+
flush();
235+
fixture.detectChanges();
236+
237+
expect(inputNativeElement.getAttribute('aria-describedby')).toEqual('a b c');
238+
}));
239+
240+
it('should set aria-describedby correctly when an empty list of ids is passed to setDescribedByIds', fakeAsync(() => {
241+
const ids: string[] = [];
242+
243+
testChipInput.chipGridInstance.setDescribedByIds(ids);
244+
flush();
245+
fixture.detectChanges();
246+
247+
expect(inputNativeElement.getAttribute('aria-describedby')).toBeNull();
248+
}));
233249
});
234250
});
235251

src/material-experimental/mdc-chips/chip-input.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ let nextUniqueId = 0;
6565
'[attr.disabled]': 'disabled || null',
6666
'[attr.placeholder]': 'placeholder || null',
6767
'[attr.aria-invalid]': '_chipGrid && _chipGrid.ngControl ? _chipGrid.ngControl.invalid : null',
68+
'[attr.aria-describedby]': '_ariaDescribedby || null',
6869
'[attr.aria-required]': '_chipGrid && _chipGrid.required || null',
6970
'[attr.required]': '_chipGrid && _chipGrid.required || null',
7071
},
@@ -73,6 +74,9 @@ export class MatChipInput implements MatChipTextControl, AfterContentInit, OnCha
7374
/** Used to prevent focus moving to chips while user is holding backspace */
7475
private _focusLastChipOnBackspace: boolean;
7576

77+
/** Value for ariaDescribedby property */
78+
_ariaDescribedby?: string;
79+
7680
/** Whether the control is focused. */
7781
focused: boolean = false;
7882
_chipGrid: MatChipGrid;
@@ -240,6 +244,10 @@ export class MatChipInput implements MatChipTextControl, AfterContentInit, OnCha
240244
this._focusLastChipOnBackspace = true;
241245
}
242246

247+
setDescribedByIds(ids: string[]): void {
248+
this._ariaDescribedby = ids.join(' ');
249+
}
250+
243251
/** Checks whether a keycode is one of the configured separators. */
244252
private _isSeparatorKey(event: KeyboardEvent) {
245253
return !hasModifierKey(event) && new Set(this.separatorKeyCodes).has(event.keyCode);

src/material-experimental/mdc-chips/chip-set.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,6 @@ const _MatChipSetMixinBase = mixinTabIndex(MatChipSetBase);
6565
host: {
6666
'class': 'mat-mdc-chip-set mdc-evolution-chip-set',
6767
'[attr.role]': 'role',
68-
// TODO: replace this binding with use of AriaDescriber
69-
'[attr.aria-describedby]': '_ariaDescribedby || null',
7068
},
7169
encapsulation: ViewEncapsulation.None,
7270
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -138,9 +136,6 @@ export class MatChipSet
138136
},
139137
};
140138

141-
/** The aria-describedby attribute on the chip list for improved a11y. */
142-
_ariaDescribedby: string;
143-
144139
/**
145140
* Map from class to whether the class is enabled.
146141
* Enabled classes are set on the MDC chip-set div.

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,7 @@ export interface MatChipTextControl {
2222

2323
/** Focuses the text control. */
2424
focus(): void;
25+
26+
/** Sets the list of ids the input is described by. */
27+
setDescribedByIds(ids: string[]): void;
2528
}

0 commit comments

Comments
 (0)