From 518022288b268dff2125017f1f187ef1683527a2 Mon Sep 17 00:00:00 2001 From: ByzantineFailure Date: Fri, 25 Mar 2022 14:05:36 -0700 Subject: [PATCH] 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 1bc98ec31eab37debe94bb1cee2618805b82c832) --- .../mdc-chips/chip-grid.spec.ts | 29 ++++++-- .../mdc-chips/chip-grid.ts | 21 +++++- .../mdc-chips/chip-input.spec.ts | 74 +++++++++++-------- .../mdc-chips/chip-input.ts | 8 ++ .../mdc-chips/chip-set.ts | 5 -- .../mdc-chips/chip-text-control.ts | 3 + 6 files changed, 96 insertions(+), 44 deletions(-) diff --git a/src/material-experimental/mdc-chips/chip-grid.spec.ts b/src/material-experimental/mdc-chips/chip-grid.spec.ts index 96e44bcb7d25..6cca4302482a 100644 --- a/src/material-experimental/mdc-chips/chip-grid.spec.ts +++ b/src/material-experimental/mdc-chips/chip-grid.spec.ts @@ -846,13 +846,18 @@ describe('MDC-based MatChipGrid', () => { let errorTestComponent: ChipGridWithFormErrorMessages; let containerEl: HTMLElement; let chipGridEl: HTMLElement; + let inputEl: HTMLElement; - beforeEach(() => { + beforeEach(fakeAsync(() => { fixture = createComponent(ChipGridWithFormErrorMessages); + flush(); + fixture.detectChanges(); + errorTestComponent = fixture.componentInstance; containerEl = fixture.debugElement.query(By.css('mat-form-field'))!.nativeElement; chipGridEl = fixture.debugElement.query(By.css('mat-chip-grid'))!.nativeElement; - }); + inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement; + })); it('should not show any errors if the user has not interacted', () => { expect(errorTestComponent.formControl.untouched) @@ -901,6 +906,7 @@ describe('MDC-based MatChipGrid', () => { .toBe(0); dispatchFakeEvent(fixture.debugElement.query(By.css('form'))!.nativeElement, 'submit'); + flush(); fixture.detectChanges(); fixture.whenStable().then(() => { @@ -917,10 +923,12 @@ describe('MDC-based MatChipGrid', () => { .withContext('Expected aria-invalid to be set to "true".') .toBe('true'); }); + flush(); })); it('should hide the errors and show the hints once the chip grid becomes valid', fakeAsync(() => { errorTestComponent.formControl.markAsTouched(); + flush(); fixture.detectChanges(); fixture.whenStable().then(() => { @@ -935,6 +943,7 @@ describe('MDC-based MatChipGrid', () => { .toBe(0); errorTestComponent.formControl.setValue('something'); + flush(); fixture.detectChanges(); fixture.whenStable().then(() => { @@ -949,6 +958,8 @@ describe('MDC-based MatChipGrid', () => { .withContext('Expected one hint to be shown once the input is valid.') .toBe(1); }); + + flush(); }); })); @@ -959,11 +970,11 @@ describe('MDC-based MatChipGrid', () => { expect(containerEl.querySelector('mat-error')!.getAttribute('aria-live')).toBe('polite'); }); - it('sets the aria-describedby to reference errors when in error state', () => { + it('sets the aria-describedby on the input to reference errors when in error state', fakeAsync(() => { let hintId = fixture.debugElement .query(By.css('.mat-mdc-form-field-hint'))! .nativeElement.getAttribute('id'); - let describedBy = chipGridEl.getAttribute('aria-describedby'); + let describedBy = inputEl.getAttribute('aria-describedby'); expect(hintId).withContext('hint should be shown').toBeTruthy(); expect(describedBy).toBe(hintId); @@ -971,15 +982,19 @@ describe('MDC-based MatChipGrid', () => { fixture.componentInstance.formControl.markAsTouched(); fixture.detectChanges(); + // Flush the describedby timer and detect changes caused by it. + flush(); + fixture.detectChanges(); + let errorIds = fixture.debugElement .queryAll(By.css('.mat-mdc-form-field-error')) .map(el => el.nativeElement.getAttribute('id')) .join(' '); - describedBy = chipGridEl.getAttribute('aria-describedby'); + let errorDescribedBy = inputEl.getAttribute('aria-describedby'); expect(errorIds).withContext('errors should be shown').toBeTruthy(); - expect(describedBy).toBe(errorIds); - }); + expect(errorDescribedBy).toBe(errorIds); + })); }); function createComponent( diff --git a/src/material-experimental/mdc-chips/chip-grid.ts b/src/material-experimental/mdc-chips/chip-grid.ts index 57add5da8653..911a32ca4105 100644 --- a/src/material-experimental/mdc-chips/chip-grid.ts +++ b/src/material-experimental/mdc-chips/chip-grid.ts @@ -97,8 +97,6 @@ const _MatChipGridMixinBase = mixinErrorState(MatChipGridBase); 'class': 'mat-mdc-chip-set mat-mdc-chip-grid mdc-evolution-chip-set', '[attr.role]': 'role', '[tabIndex]': '_chips && _chips.length === 0 ? -1 : tabIndex', - // TODO: replace this binding with use of AriaDescriber - '[attr.aria-describedby]': '_ariaDescribedby || null', '[attr.aria-disabled]': 'disabled.toString()', '[attr.aria-invalid]': 'errorState', '[class.mat-mdc-chip-list-disabled]': 'disabled', @@ -132,6 +130,11 @@ export class MatChipGrid /** The chip input to add more chips */ protected _chipInput: MatChipTextControl; + /** + * List of element ids to propagate to the chipInput's aria-describedby attribute. + */ + private _ariaDescribedbyIds: string[] = []; + /** * Function when touched. Set as part of ControlValueAccessor implementation. * @docs-private @@ -329,6 +332,7 @@ export class MatChipGrid /** Associates an HTML input element with this chip grid. */ registerInput(inputElement: MatChipTextControl): void { this._chipInput = inputElement; + this._chipInput.setDescribedByIds(this._ariaDescribedbyIds); } /** @@ -370,7 +374,18 @@ export class MatChipGrid * @docs-private */ setDescribedByIds(ids: string[]) { - this._ariaDescribedby = ids.join(' '); + // We must keep this up to date to handle the case where ids are set + // before the chip input is registered. + this._ariaDescribedbyIds = ids; + + if (this._chipInput) { + // Use a setTimeout in case this is being run during change detection + // and the chip input has already determined its host binding for + // aria-describedBy. + setTimeout(() => { + this._chipInput.setDescribedByIds(ids); + }, 0); + } } /** diff --git a/src/material-experimental/mdc-chips/chip-input.spec.ts b/src/material-experimental/mdc-chips/chip-input.spec.ts index e83af9f8d9f6..46c6b52d9505 100644 --- a/src/material-experimental/mdc-chips/chip-input.spec.ts +++ b/src/material-experimental/mdc-chips/chip-input.spec.ts @@ -25,39 +25,35 @@ describe('MDC-based MatChipInput', () => { let chipInputDirective: MatChipInput; let dir = 'ltr'; - beforeEach( - waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [PlatformModule, MatChipsModule, MatFormFieldModule, NoopAnimationsModule], - declarations: [TestChipInput], - providers: [ - { - provide: Directionality, - useFactory: () => { - return { - value: dir.toLowerCase(), - change: new Subject(), - }; - }, + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [PlatformModule, MatChipsModule, MatFormFieldModule, NoopAnimationsModule], + declarations: [TestChipInput], + providers: [ + { + provide: Directionality, + useFactory: () => { + return { + value: dir.toLowerCase(), + change: new Subject(), + }; }, - ], - }); + }, + ], + }); - TestBed.compileComponents(); - }), - ); + TestBed.compileComponents(); + })); - beforeEach( - waitForAsync(() => { - fixture = TestBed.createComponent(TestChipInput); - testChipInput = fixture.debugElement.componentInstance; - fixture.detectChanges(); + beforeEach(waitForAsync(() => { + fixture = TestBed.createComponent(TestChipInput); + testChipInput = fixture.debugElement.componentInstance; + fixture.detectChanges(); - inputDebugElement = fixture.debugElement.query(By.directive(MatChipInput))!; - chipInputDirective = inputDebugElement.injector.get(MatChipInput); - inputNativeElement = inputDebugElement.nativeElement; - }), - ); + inputDebugElement = fixture.debugElement.query(By.directive(MatChipInput))!; + chipInputDirective = inputDebugElement.injector.get(MatChipInput); + inputNativeElement = inputDebugElement.nativeElement; + })); describe('basic behavior', () => { it('emits the (chipEnd) on enter keyup', () => { @@ -230,6 +226,26 @@ describe('MDC-based MatChipInput', () => { dispatchKeyboardEvent(inputNativeElement, 'keydown', ENTER, undefined, {shift: true}); expect(testChipInput.add).not.toHaveBeenCalled(); }); + + it('should set aria-describedby correctly when a non-empty list of ids is passed to setDescribedByIds', fakeAsync(() => { + const ids = ['a', 'b', 'c']; + + testChipInput.chipGridInstance.setDescribedByIds(ids); + flush(); + fixture.detectChanges(); + + expect(inputNativeElement.getAttribute('aria-describedby')).toEqual('a b c'); + })); + + it('should set aria-describedby correctly when an empty list of ids is passed to setDescribedByIds', fakeAsync(() => { + const ids: string[] = []; + + testChipInput.chipGridInstance.setDescribedByIds(ids); + flush(); + fixture.detectChanges(); + + expect(inputNativeElement.getAttribute('aria-describedby')).toBeNull(); + })); }); }); diff --git a/src/material-experimental/mdc-chips/chip-input.ts b/src/material-experimental/mdc-chips/chip-input.ts index d69d027e1f72..c38debeae9b3 100644 --- a/src/material-experimental/mdc-chips/chip-input.ts +++ b/src/material-experimental/mdc-chips/chip-input.ts @@ -68,6 +68,7 @@ let nextUniqueId = 0; '[attr.disabled]': 'disabled || null', '[attr.placeholder]': 'placeholder || null', '[attr.aria-invalid]': '_chipGrid && _chipGrid.ngControl ? _chipGrid.ngControl.invalid : null', + '[attr.aria-describedby]': '_ariaDescribedby || null', '[attr.aria-required]': '_chipGrid && _chipGrid.required || null', '[attr.required]': '_chipGrid && _chipGrid.required || null', }, @@ -76,6 +77,9 @@ export class MatChipInput implements MatChipTextControl, AfterContentInit, OnCha /** Used to prevent focus moving to chips while user is holding backspace */ private _focusLastChipOnBackspace: boolean; + /** Value for ariaDescribedby property */ + _ariaDescribedby?: string; + /** Whether the control is focused. */ focused: boolean = false; _chipGrid: MatChipGrid; @@ -243,6 +247,10 @@ export class MatChipInput implements MatChipTextControl, AfterContentInit, OnCha this._focusLastChipOnBackspace = true; } + setDescribedByIds(ids: string[]): void { + this._ariaDescribedby = ids.join(' '); + } + /** Checks whether a keycode is one of the configured separators. */ private _isSeparatorKey(event: KeyboardEvent) { return !hasModifierKey(event) && new Set(this.separatorKeyCodes).has(event.keyCode); diff --git a/src/material-experimental/mdc-chips/chip-set.ts b/src/material-experimental/mdc-chips/chip-set.ts index 1b8787de063a..2edea14af562 100644 --- a/src/material-experimental/mdc-chips/chip-set.ts +++ b/src/material-experimental/mdc-chips/chip-set.ts @@ -65,8 +65,6 @@ const _MatChipSetMixinBase = mixinTabIndex(MatChipSetBase); host: { 'class': 'mat-mdc-chip-set mdc-evolution-chip-set', '[attr.role]': 'role', - // TODO: replace this binding with use of AriaDescriber - '[attr.aria-describedby]': '_ariaDescribedby || null', }, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, @@ -135,9 +133,6 @@ export class MatChipSet }, }; - /** The aria-describedby attribute on the chip list for improved a11y. */ - _ariaDescribedby: string; - /** * Map from class to whether the class is enabled. * Enabled classes are set on the MDC chip-set div. diff --git a/src/material-experimental/mdc-chips/chip-text-control.ts b/src/material-experimental/mdc-chips/chip-text-control.ts index bcb515d85a54..422328517e23 100644 --- a/src/material-experimental/mdc-chips/chip-text-control.ts +++ b/src/material-experimental/mdc-chips/chip-text-control.ts @@ -22,4 +22,7 @@ export interface MatChipTextControl { /** Focuses the text control. */ focus(): void; + + /** Sets the list of ids the input is described by. */ + setDescribedByIds(ids: string[]): void; }