From dcf2fac0420116236c29c31573c577eac99d68b0 Mon Sep 17 00:00:00 2001 From: Zach Arend Date: Thu, 8 Dec 2022 17:16:24 -0800 Subject: [PATCH] fix(material/chips): implement ariaDescription with aria-describedby (#26105) For the `ariaDescription` Input, implement with aria-describedby rather than aria-description. aria-description is still in W3C Editor's Draft for ARIA 1.3. --- src/material/chips/chip-option.html | 4 +++- src/material/chips/chip-option.spec.ts | 24 ++++++++++++++++++++-- src/material/chips/chip-row.html | 4 +++- src/material/chips/chip-row.spec.ts | 26 +++++++++++++++++++++--- src/material/chips/chip.ts | 9 ++++++++ tools/public_api_guard/material/chips.md | 1 + 6 files changed, 61 insertions(+), 7 deletions(-) diff --git a/src/material/chips/chip-option.html b/src/material/chips/chip-option.html index 0456dd9264c3..41c408c4470b 100644 --- a/src/material/chips/chip-option.html +++ b/src/material/chips/chip-option.html @@ -11,7 +11,7 @@ [_allowFocusWhenDisabled]="true" [attr.aria-selected]="ariaSelected" [attr.aria-label]="ariaLabel" - [attr.aria-description]="ariaDescription" + [attr.aria-describedby]="_ariaDescriptionId" role="option"> @@ -34,3 +34,5 @@ *ngIf="_hasTrailingIcon()"> + +{{ariaDescription}} \ No newline at end of file diff --git a/src/material/chips/chip-option.spec.ts b/src/material/chips/chip-option.spec.ts index 4ee1dd1ecbb3..43b8d56cdc40 100644 --- a/src/material/chips/chip-option.spec.ts +++ b/src/material/chips/chip-option.spec.ts @@ -308,8 +308,28 @@ describe('MDC-based Option Chips', () => { .withContext('expected to find an element with option role') .toBeTruthy(); - expect(optionElement.getAttribute('aria-label')).toBe('option name'); - expect(optionElement.getAttribute('aria-description')).toBe('option description'); + expect(optionElement.getAttribute('aria-label')).toMatch(/option name/i); + + const optionElementDescribedBy = optionElement!.getAttribute('aria-describedby'); + expect(optionElementDescribedBy) + .withContext('expected primary grid cell to have a non-empty aria-describedby attribute') + .toBeTruthy(); + + const optionElementDescriptions = Array.from( + (fixture.nativeElement as HTMLElement).querySelectorAll( + optionElementDescribedBy! + .split(/\s+/g) + .map(x => `#${x}`) + .join(','), + ), + ); + + const optionElementDescription = optionElementDescriptions + .map(x => x.textContent?.trim()) + .join(' ') + .trim(); + + expect(optionElementDescription).toMatch(/option description/i); }); }); diff --git a/src/material/chips/chip-row.html b/src/material/chips/chip-row.html index ea013feaffa1..218fdfc181d9 100644 --- a/src/material/chips/chip-row.html +++ b/src/material/chips/chip-row.html @@ -14,7 +14,7 @@ [tabIndex]="tabIndex" [disabled]="disabled" [attr.aria-label]="ariaLabel" - [attr.aria-description]="ariaDescription"> + [attr.aria-describedby]="_ariaDescriptionId"> @@ -38,3 +38,5 @@ *ngIf="_hasTrailingIcon()"> + +{{ariaDescription}} diff --git a/src/material/chips/chip-row.spec.ts b/src/material/chips/chip-row.spec.ts index 20b198679ba3..1c4e69390fac 100644 --- a/src/material/chips/chip-row.spec.ts +++ b/src/material/chips/chip-row.spec.ts @@ -340,15 +340,35 @@ describe('MDC-based Row Chips', () => { fixture.detectChanges(); - const primaryGridCell = fixture.nativeElement.querySelector( + const primaryGridCell = (fixture.nativeElement as HTMLElement).querySelector( '[role="gridcell"].mdc-evolution-chip__cell--primary .mat-mdc-chip-action', ); expect(primaryGridCell) .withContext('expected to find the grid cell for the primary chip action') .toBeTruthy(); - expect(primaryGridCell.getAttribute('aria-label')).toBe('chip name'); - expect(primaryGridCell.getAttribute('aria-description')).toBe('chip description'); + expect(primaryGridCell!.getAttribute('aria-label')).toMatch(/chip name/i); + + const primaryGridCellDescribedBy = primaryGridCell!.getAttribute('aria-describedby'); + expect(primaryGridCellDescribedBy) + .withContext('expected primary grid cell to have a non-empty aria-describedby attribute') + .toBeTruthy(); + + const primaryGridCellDescriptions = Array.from( + (fixture.nativeElement as HTMLElement).querySelectorAll( + primaryGridCellDescribedBy! + .split(/\s+/g) + .map(x => `#${x}`) + .join(','), + ), + ); + + const primaryGridCellDescription = primaryGridCellDescriptions + .map(x => x.textContent?.trim()) + .join(' ') + .trim(); + + expect(primaryGridCellDescription).toMatch(/chip description/i); }); }); }); diff --git a/src/material/chips/chip.ts b/src/material/chips/chip.ts index 66e975061eb5..d1457b7b9f42 100644 --- a/src/material/chips/chip.ts +++ b/src/material/chips/chip.ts @@ -147,12 +147,21 @@ export class MatChip /** A unique id for the chip. If none is supplied, it will be auto-generated. */ @Input() id: string = `mat-mdc-chip-${uid++}`; + // TODO(#26104): Consider deprecating and using `_computeAriaAccessibleName` instead. + // `ariaLabel` may be unnecessary, and `_computeAriaAccessibleName` only supports + // datepicker's use case. /** ARIA label for the content of the chip. */ @Input('aria-label') ariaLabel: string | null = null; + // TODO(#26104): Consider deprecating and using `_computeAriaAccessibleName` instead. + // `ariaDescription` may be unnecessary, and `_computeAriaAccessibleName` only supports + // datepicker's use case. /** ARIA description for the content of the chip. */ @Input('aria-description') ariaDescription: string | null = null; + /** Id of a span that contains this chip's aria description. */ + _ariaDescriptionId = `${this.id}-aria-description`; + private _textElement!: HTMLElement; /** diff --git a/tools/public_api_guard/material/chips.md b/tools/public_api_guard/material/chips.md index 9575408cbb29..fd298df7f983 100644 --- a/tools/public_api_guard/material/chips.md +++ b/tools/public_api_guard/material/chips.md @@ -65,6 +65,7 @@ export class MatChip extends _MatChipMixinBase implements AfterViewInit, CanColo constructor(_changeDetectorRef: ChangeDetectorRef, elementRef: ElementRef, _ngZone: NgZone, _focusMonitor: FocusMonitor, _document: any, animationMode?: string, _globalRippleOptions?: RippleGlobalOptions | undefined, tabIndex?: string); _animationsDisabled: boolean; ariaDescription: string | null; + _ariaDescriptionId: string; ariaLabel: string | null; protected basicChipAttrName: string; // (undocumented)