Skip to content

Commit 5180222

Browse files
ByzantineFailureandrewseguin
authored andcommitted
fix(material-experimental/mdc-chips): Mirror aria-describedby to matChipInput (#24551)
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 (cherry picked from commit 1bc98ec)
1 parent 20af3e7 commit 5180222

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
@@ -846,13 +846,18 @@ describe('MDC-based MatChipGrid', () => {
846846
let errorTestComponent: ChipGridWithFormErrorMessages;
847847
let containerEl: HTMLElement;
848848
let chipGridEl: HTMLElement;
849+
let inputEl: HTMLElement;
849850

850-
beforeEach(() => {
851+
beforeEach(fakeAsync(() => {
851852
fixture = createComponent(ChipGridWithFormErrorMessages);
853+
flush();
854+
fixture.detectChanges();
855+
852856
errorTestComponent = fixture.componentInstance;
853857
containerEl = fixture.debugElement.query(By.css('mat-form-field'))!.nativeElement;
854858
chipGridEl = fixture.debugElement.query(By.css('mat-chip-grid'))!.nativeElement;
855-
});
859+
inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
860+
}));
856861

857862
it('should not show any errors if the user has not interacted', () => {
858863
expect(errorTestComponent.formControl.untouched)
@@ -901,6 +906,7 @@ describe('MDC-based MatChipGrid', () => {
901906
.toBe(0);
902907

903908
dispatchFakeEvent(fixture.debugElement.query(By.css('form'))!.nativeElement, 'submit');
909+
flush();
904910
fixture.detectChanges();
905911

906912
fixture.whenStable().then(() => {
@@ -917,10 +923,12 @@ describe('MDC-based MatChipGrid', () => {
917923
.withContext('Expected aria-invalid to be set to "true".')
918924
.toBe('true');
919925
});
926+
flush();
920927
}));
921928

922929
it('should hide the errors and show the hints once the chip grid becomes valid', fakeAsync(() => {
923930
errorTestComponent.formControl.markAsTouched();
931+
flush();
924932
fixture.detectChanges();
925933

926934
fixture.whenStable().then(() => {
@@ -935,6 +943,7 @@ describe('MDC-based MatChipGrid', () => {
935943
.toBe(0);
936944

937945
errorTestComponent.formControl.setValue('something');
946+
flush();
938947
fixture.detectChanges();
939948

940949
fixture.whenStable().then(() => {
@@ -949,6 +958,8 @@ describe('MDC-based MatChipGrid', () => {
949958
.withContext('Expected one hint to be shown once the input is valid.')
950959
.toBe(1);
951960
});
961+
962+
flush();
952963
});
953964
}));
954965

@@ -959,27 +970,31 @@ describe('MDC-based MatChipGrid', () => {
959970
expect(containerEl.querySelector('mat-error')!.getAttribute('aria-live')).toBe('polite');
960971
});
961972

962-
it('sets the aria-describedby to reference errors when in error state', () => {
973+
it('sets the aria-describedby on the input to reference errors when in error state', fakeAsync(() => {
963974
let hintId = fixture.debugElement
964975
.query(By.css('.mat-mdc-form-field-hint'))!
965976
.nativeElement.getAttribute('id');
966-
let describedBy = chipGridEl.getAttribute('aria-describedby');
977+
let describedBy = inputEl.getAttribute('aria-describedby');
967978

968979
expect(hintId).withContext('hint should be shown').toBeTruthy();
969980
expect(describedBy).toBe(hintId);
970981

971982
fixture.componentInstance.formControl.markAsTouched();
972983
fixture.detectChanges();
973984

985+
// Flush the describedby timer and detect changes caused by it.
986+
flush();
987+
fixture.detectChanges();
988+
974989
let errorIds = fixture.debugElement
975990
.queryAll(By.css('.mat-mdc-form-field-error'))
976991
.map(el => el.nativeElement.getAttribute('id'))
977992
.join(' ');
978-
describedBy = chipGridEl.getAttribute('aria-describedby');
993+
let errorDescribedBy = inputEl.getAttribute('aria-describedby');
979994

980995
expect(errorIds).withContext('errors should be shown').toBeTruthy();
981-
expect(describedBy).toBe(errorIds);
982-
});
996+
expect(errorDescribedBy).toBe(errorIds);
997+
}));
983998
});
984999

9851000
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
@@ -97,8 +97,6 @@ const _MatChipGridMixinBase = mixinErrorState(MatChipGridBase);
9797
'class': 'mat-mdc-chip-set mat-mdc-chip-grid mdc-evolution-chip-set',
9898
'[attr.role]': 'role',
9999
'[tabIndex]': '_chips && _chips.length === 0 ? -1 : tabIndex',
100-
// TODO: replace this binding with use of AriaDescriber
101-
'[attr.aria-describedby]': '_ariaDescribedby || null',
102100
'[attr.aria-disabled]': 'disabled.toString()',
103101
'[attr.aria-invalid]': 'errorState',
104102
'[class.mat-mdc-chip-list-disabled]': 'disabled',
@@ -132,6 +130,11 @@ export class MatChipGrid
132130
/** The chip input to add more chips */
133131
protected _chipInput: MatChipTextControl;
134132

133+
/**
134+
* List of element ids to propagate to the chipInput's aria-describedby attribute.
135+
*/
136+
private _ariaDescribedbyIds: string[] = [];
137+
135138
/**
136139
* Function when touched. Set as part of ControlValueAccessor implementation.
137140
* @docs-private
@@ -329,6 +332,7 @@ export class MatChipGrid
329332
/** Associates an HTML input element with this chip grid. */
330333
registerInput(inputElement: MatChipTextControl): void {
331334
this._chipInput = inputElement;
335+
this._chipInput.setDescribedByIds(this._ariaDescribedbyIds);
332336
}
333337

334338
/**
@@ -370,7 +374,18 @@ export class MatChipGrid
370374
* @docs-private
371375
*/
372376
setDescribedByIds(ids: string[]) {
373-
this._ariaDescribedby = ids.join(' ');
377+
// We must keep this up to date to handle the case where ids are set
378+
// before the chip input is registered.
379+
this._ariaDescribedbyIds = ids;
380+
381+
if (this._chipInput) {
382+
// Use a setTimeout in case this is being run during change detection
383+
// and the chip input has already determined its host binding for
384+
// aria-describedBy.
385+
setTimeout(() => {
386+
this._chipInput.setDescribedByIds(ids);
387+
}, 0);
388+
}
374389
}
375390

376391
/**

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
@@ -68,6 +68,7 @@ let nextUniqueId = 0;
6868
'[attr.disabled]': 'disabled || null',
6969
'[attr.placeholder]': 'placeholder || null',
7070
'[attr.aria-invalid]': '_chipGrid && _chipGrid.ngControl ? _chipGrid.ngControl.invalid : null',
71+
'[attr.aria-describedby]': '_ariaDescribedby || null',
7172
'[attr.aria-required]': '_chipGrid && _chipGrid.required || null',
7273
'[attr.required]': '_chipGrid && _chipGrid.required || null',
7374
},
@@ -76,6 +77,9 @@ export class MatChipInput implements MatChipTextControl, AfterContentInit, OnCha
7677
/** Used to prevent focus moving to chips while user is holding backspace */
7778
private _focusLastChipOnBackspace: boolean;
7879

80+
/** Value for ariaDescribedby property */
81+
_ariaDescribedby?: string;
82+
7983
/** Whether the control is focused. */
8084
focused: boolean = false;
8185
_chipGrid: MatChipGrid;
@@ -243,6 +247,10 @@ export class MatChipInput implements MatChipTextControl, AfterContentInit, OnCha
243247
this._focusLastChipOnBackspace = true;
244248
}
245249

250+
setDescribedByIds(ids: string[]): void {
251+
this._ariaDescribedby = ids.join(' ');
252+
}
253+
246254
/** Checks whether a keycode is one of the configured separators. */
247255
private _isSeparatorKey(event: KeyboardEvent) {
248256
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,
@@ -135,9 +133,6 @@ export class MatChipSet
135133
},
136134
};
137135

138-
/** The aria-describedby attribute on the chip list for improved a11y. */
139-
_ariaDescribedby: string;
140-
141136
/**
142137
* Map from class to whether the class is enabled.
143138
* 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)