Skip to content

Commit

Permalink
fix(material/chips): implement ariaDescription with aria-describedby
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
zarend committed Dec 5, 2022
1 parent f99af6d commit 5341bc4
Show file tree
Hide file tree
Showing 8 changed files with 69 additions and 7 deletions.
4 changes: 3 additions & 1 deletion src/material/chips/chip-option.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
[_allowFocusWhenDisabled]="true"
[attr.aria-selected]="ariaSelected"
[attr.aria-label]="ariaLabel"
[attr.aria-description]="ariaDescription"
[attr.aria-describedby]="_ariaDescriptionId"
role="option">
<span class="mdc-evolution-chip__graphic mat-mdc-chip-graphic">
<ng-content select="mat-chip-avatar, [matChipAvatar]"></ng-content>
Expand All @@ -34,3 +34,5 @@
*ngIf="_hasTrailingIcon()">
<ng-content select="mat-chip-trailing-icon,[matChipRemove],[matChipTrailingIcon]"></ng-content>
</span>

<span class="cdk-visually-hidden" [id]="_ariaDescriptionId">{{ariaDescription}}</span>
24 changes: 22 additions & 2 deletions src/material/chips/chip-option.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});

Expand Down
5 changes: 5 additions & 0 deletions src/material/chips/chip-option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export class MatChipSelectionChange {
) {}
}

let id = 1;

/**
* An extension of the MatChip component that supports chip selection. Used with MatChipListbox.
*
Expand Down Expand Up @@ -178,4 +180,7 @@ export class MatChipOption extends MatChip implements OnInit {
this._changeDetectorRef.markForCheck();
}
}

/** Id of a span that contains this chip's aria description. @docs-private */
_ariaDescriptionId = `chip-option-${id++}-aria-description`;
}
4 changes: 3 additions & 1 deletion src/material/chips/chip-row.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
[tabIndex]="tabIndex"
[disabled]="disabled"
[attr.aria-label]="ariaLabel"
[attr.aria-description]="ariaDescription">
[attr.aria-describedby]="_ariaDescriptionId">
<span class="mdc-evolution-chip__graphic mat-mdc-chip-graphic" *ngIf="leadingIcon">
<ng-content select="mat-chip-avatar, [matChipAvatar]"></ng-content>
</span>
Expand All @@ -38,3 +38,5 @@
*ngIf="_hasTrailingIcon()">
<ng-content select="mat-chip-trailing-icon,[matChipRemove],[matChipTrailingIcon]"></ng-content>
</span>

<span class="cdk-visually-hidden" [id]="_ariaDescriptionId">{{ariaDescription}}</span>
26 changes: 23 additions & 3 deletions src/material/chips/chip-row.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
Expand Down
5 changes: 5 additions & 0 deletions src/material/chips/chip-row.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import {MatChipEditInput} from './chip-edit-input';
import {takeUntil} from 'rxjs/operators';
import {MAT_CHIP} from './tokens';

let id = 1;

/** Represents an event fired on an individual `mat-chip` when it is edited. */
export interface MatChipEditedEvent extends MatChipEvent {
/** The final edit value. */
Expand Down Expand Up @@ -212,4 +214,7 @@ export class MatChipRow extends MatChip implements AfterViewInit {
private _getEditInput(): MatChipEditInput {
return this.contentEditInput || this.defaultEditInput!;
}

/** Id of a span that contains this chip's aria description. @docs-private */
_ariaDescriptionId = `chip-row-${id++}-aria-description`;
}
6 changes: 6 additions & 0 deletions src/material/chips/chip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,15 @@ 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;

Expand Down
2 changes: 2 additions & 0 deletions tools/public_api_guard/material/chips.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ export class MatChipListboxChange {

// @public
export class MatChipOption extends MatChip implements OnInit {
_ariaDescriptionId: string;
get ariaSelected(): string | null;
protected basicChipAttrName: string;
_chipListMultiple: boolean;
Expand Down Expand Up @@ -376,6 +377,7 @@ export class MatChipRemove extends MatChipAction {
// @public
export class MatChipRow extends MatChip implements AfterViewInit {
constructor(changeDetectorRef: ChangeDetectorRef, elementRef: ElementRef, ngZone: NgZone, focusMonitor: FocusMonitor, _document: any, animationMode?: string, globalRippleOptions?: RippleGlobalOptions, tabIndex?: string);
_ariaDescriptionId: string;
// (undocumented)
protected basicChipAttrName: string;
contentEditInput?: MatChipEditInput;
Expand Down

0 comments on commit 5341bc4

Please sign in to comment.