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)