Skip to content

Commit

Permalink
fix(material-experimental/mdc-chips): Mirror aria-describedby to matC…
Browse files Browse the repository at this point in the history
…hipInput

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
  • Loading branch information
ByzantineFailure committed Mar 21, 2022
1 parent 9371606 commit 4ec71a4
Show file tree
Hide file tree
Showing 6 changed files with 48 additions and 11 deletions.
8 changes: 5 additions & 3 deletions src/material-experimental/mdc-chips/chip-grid.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
17 changes: 14 additions & 3 deletions src/material-experimental/mdc-chips/chip-grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -378,7 +382,14 @@ 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);
console.warn('Set chip input ids to ' + ids);
}
}

/**
Expand Down
18 changes: 18 additions & 0 deletions src/material-experimental/mdc-chips/chip-input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});

Expand Down
8 changes: 8 additions & 0 deletions src/material-experimental/mdc-chips/chip-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 0 additions & 5 deletions src/material-experimental/mdc-chips/chip-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions src/material-experimental/mdc-chips/chip-text-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

0 comments on commit 4ec71a4

Please sign in to comment.