From 195cbfa324ea5933c3cab878551f9ceed94eafb9 Mon Sep 17 00:00:00 2001 From: Michael Eaton Date: Tue, 8 Mar 2022 10:43:53 -0800 Subject: [PATCH] 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 --- .../mdc-chips/chip-grid.spec.ts | 8 +++++--- .../mdc-chips/chip-grid.ts | 16 +++++++++++++--- .../mdc-chips/chip-input.spec.ts | 18 ++++++++++++++++++ .../mdc-chips/chip-input.ts | 8 ++++++++ .../mdc-chips/chip-set.ts | 5 ----- .../mdc-chips/chip-text-control.ts | 3 +++ 6 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/material-experimental/mdc-chips/chip-grid.spec.ts b/src/material-experimental/mdc-chips/chip-grid.spec.ts index 3208ac3e8a96..3ad5a63b304b 100644 --- a/src/material-experimental/mdc-chips/chip-grid.spec.ts +++ b/src/material-experimental/mdc-chips/chip-grid.spec.ts @@ -853,12 +853,14 @@ describe('MDC-based MatChipGrid', () => { let errorTestComponent: ChipGridWithFormErrorMessages; let containerEl: HTMLElement; let chipGridEl: HTMLElement; + let inputEl: HTMLElement; beforeEach(() => { fixture = createComponent(ChipGridWithFormErrorMessages); 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', () => { @@ -966,11 +968,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', () => { 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); @@ -982,7 +984,7 @@ describe('MDC-based MatChipGrid', () => { .queryAll(By.css('.mat-mdc-form-field-error')) .map(el => el.nativeElement.getAttribute('id')) .join(' '); - describedBy = chipGridEl.getAttribute('aria-describedby'); + describedBy = inputEl.getAttribute('aria-describedby'); expect(errorIds).withContext('errors should be shown').toBeTruthy(); expect(describedBy).toBe(errorIds); diff --git a/src/material-experimental/mdc-chips/chip-grid.ts b/src/material-experimental/mdc-chips/chip-grid.ts index b1370f6bcc69..8aba6e537732 100644 --- a/src/material-experimental/mdc-chips/chip-grid.ts +++ b/src/material-experimental/mdc-chips/chip-grid.ts @@ -108,8 +108,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', @@ -145,6 +143,11 @@ export class MatChipGrid protected override _defaultRole = 'grid'; + /** + * 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 @@ -337,6 +340,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); } /** @@ -378,7 +382,13 @@ 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) { + this._chipInput.setDescribedByIds(ids); + } } /** diff --git a/src/material-experimental/mdc-chips/chip-input.spec.ts b/src/material-experimental/mdc-chips/chip-input.spec.ts index e83af9f8d9f6..9bfa47b33e88 100644 --- a/src/material-experimental/mdc-chips/chip-input.spec.ts +++ b/src/material-experimental/mdc-chips/chip-input.spec.ts @@ -230,6 +230,24 @@ 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', () => { + const ids = ['a', 'b', 'c']; + + testChipInput.chipGridInstance.setDescribedByIds(ids); + 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', () => { + const ids: string[] = []; + + testChipInput.chipGridInstance.setDescribedByIds(ids); + 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 1362f612cf7d..deb2a519fc16 100644 --- a/src/material-experimental/mdc-chips/chip-input.ts +++ b/src/material-experimental/mdc-chips/chip-input.ts @@ -65,6 +65,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', }, @@ -73,6 +74,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; @@ -240,6 +244,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 381bcbfa6361..8552ea0e5c55 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, @@ -138,9 +136,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; }