Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
fix(material/chips): add support for aria-description
Add support for aria-description on mat-chip, mat-chip-option and
mat-chip-row. mat-chip-option and mat-chip-row put aria-desciprtion on
the same element that already has the aria-label.

aria-description is needed for when developers need to provide more
information that in the aria-label. This is especially needed for
chip-row when [editable]="true". That's because it gives a way to
communicate to screen reader users how to begin editing a chip.
  • Loading branch information
zarend committed Nov 15, 2022
commit 8c12eb41cee12b0c48ac8c343fc131ae406ec671
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
<mat-chip-row *ngFor="let fruit of fruits"
(removed)="remove(fruit)"
[editable]="true"
(edited)="edit(fruit, $event)">
(edited)="edit(fruit, $event)"
[aria-description]="'press enter to edit ' + fruit.name">
{{fruit.name}}
<button matChipRemove [attr.aria-label]="'remove ' + fruit">
<button matChipRemove [attr.aria-label]="'remove ' + fruit.name">
<mat-icon>cancel</mat-icon>
</button>
</mat-chip-row>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class ChipsInputExample {

// Edit existing fruit
const index = this.fruits.indexOf(fruit);
if (index > 0) {
if (index >= 0) {
this.fruits[index].name = value;
}
}
Expand Down
1 change: 1 addition & 0 deletions src/material/chips/chip-option.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
[_allowFocusWhenDisabled]="true"
[attr.aria-selected]="ariaSelected"
[attr.aria-label]="ariaLabel"
[attr.aria-description]="ariaDescription"
role="option">
<span class="mdc-evolution-chip__graphic mat-mdc-chip-graphic" *ngIf="_hasLeadingGraphic()">
<ng-content select="mat-chip-avatar, [matChipAvatar]"></ng-content>
Expand Down
22 changes: 21 additions & 1 deletion src/material/chips/chip-option.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,23 @@ describe('MDC-based Option Chips', () => {
});
});

describe('a11y', () => {
it('should apply `ariaLabel` and `ariaDesciption` to the element with option role', () => {
testComponent.ariaLabel = 'option name';
testComponent.ariaDescription = 'option description';

fixture.detectChanges();

const optionElement = fixture.nativeElement.querySelector('[role="option"]') as HTMLElement;
expect(optionElement)
.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');
});
});

it('should contain a focus indicator inside the text label', () => {
const label = chipNativeElement.querySelector('.mdc-evolution-chip__text-label');
expect(label?.querySelector('.mat-mdc-focus-indicator')).toBeTruthy();
Expand All @@ -310,7 +327,8 @@ describe('MDC-based Option Chips', () => {
<mat-chip-option [selectable]="selectable"
[color]="color" [selected]="selected" [disabled]="disabled"
(destroyed)="chipDestroy($event)"
(selectionChange)="chipSelectionChange($event)">
(selectionChange)="chipSelectionChange($event)"
[aria-label]="ariaLabel" [aria-description]="ariaDescription">
<span class="avatar" matChipAvatar></span>
{{name}}
</mat-chip-option>
Expand All @@ -325,6 +343,8 @@ class SingleChip {
selected: boolean = false;
selectable: boolean = true;
shouldShow: boolean = true;
ariaLabel: string | null = null;
ariaDescription: string | null = null;

chipDestroy: (event?: MatChipEvent) => void = () => {};
chipSelectionChange: (event?: MatChipSelectionChange) => void = () => {};
Expand Down
1 change: 1 addition & 0 deletions src/material/chips/chip-option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export class MatChipSelectionChange {
'[class.mat-mdc-chip-with-trailing-icon]': '_hasTrailingIcon()',
'[attr.tabindex]': 'null',
'[attr.aria-label]': 'null',
'[attr.aria-description]': 'null',
'[attr.role]': 'role',
'[id]': 'id',
},
Expand Down
3 changes: 2 additions & 1 deletion src/material/chips/chip-row.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
[attr.role]="editable ? 'button' : null"
[tabIndex]="tabIndex"
[disabled]="disabled"
[attr.aria-label]="ariaLabel">
[attr.aria-label]="ariaLabel"
[attr.aria-description]="ariaDescription">
<span class="mdc-evolution-chip__graphic mat-mdc-chip-graphic" *ngIf="leadingIcon">
<ng-content select="mat-chip-avatar, [matChipAvatar]"></ng-content>
</span>
Expand Down
24 changes: 23 additions & 1 deletion src/material/chips/chip-row.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,25 @@ describe('MDC-based Row Chips', () => {
expect(document.activeElement).not.toBe(primaryAction);
}));
});

describe('a11y', () => {
it('should apply `ariaLabel` and `ariaDesciption` to the primary gridcell', () => {
fixture.componentInstance.ariaLabel = 'chip name';
fixture.componentInstance.ariaDescription = 'chip description';

fixture.detectChanges();

const primaryGridCell = fixture.nativeElement.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');
});
});
});
});

Expand All @@ -342,7 +361,8 @@ describe('MDC-based Row Chips', () => {
<mat-chip-row [removable]="removable"
[color]="color" [disabled]="disabled" [editable]="editable"
(destroyed)="chipDestroy($event)"
(removed)="chipRemove($event)" (edited)="chipEdit($event)">
(removed)="chipRemove($event)" (edited)="chipEdit($event)"
[aria-label]="ariaLabel" [aria-description]="ariaDescription">
{{name}}
<button matChipRemove>x</button>
<span *ngIf="useCustomEditInput" class="projected-edit-input" matChipEditInput></span>
Expand All @@ -361,6 +381,8 @@ class SingleChip {
shouldShow: boolean = true;
editable: boolean = false;
useCustomEditInput: boolean = true;
ariaLabel: string | null = null;
ariaDescription: string | null = null;

chipDestroy: (event?: MatChipEvent) => void = () => {};
chipRemove: (event?: MatChipEvent) => void = () => {};
Expand Down
1 change: 1 addition & 0 deletions src/material/chips/chip-row.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export interface MatChipEditedEvent extends MatChipEvent {
'[id]': 'id',
'[attr.tabindex]': 'null',
'[attr.aria-label]': 'null',
'[attr.aria-description]': 'null',
'[attr.role]': 'role',
'(mousedown)': '_mousedown($event)',
'(dblclick)': '_doubleclick($event)',
Expand Down
3 changes: 3 additions & 0 deletions src/material/chips/chip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ export class MatChip
/** ARIA label for the content of the chip. */
@Input('aria-label') ariaLabel: string | null = null;

/** ARIA description for the content of the chip. */
@Input('aria-description') ariaDescription: string | null = null;

private _textElement!: HTMLElement;

/**
Expand Down
3 changes: 2 additions & 1 deletion tools/public_api_guard/material/chips.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const MAT_CHIPS_DEFAULT_OPTIONS: InjectionToken<MatChipsDefaultOptions>;
export class MatChip extends _MatChipMixinBase implements AfterViewInit, CanColor, CanDisableRipple, CanDisable, HasTabIndex, OnDestroy {
constructor(_changeDetectorRef: ChangeDetectorRef, elementRef: ElementRef<HTMLElement>, _ngZone: NgZone, _focusMonitor: FocusMonitor, _document: any, animationMode?: string, _globalRippleOptions?: RippleGlobalOptions | undefined, tabIndex?: string);
_animationsDisabled: boolean;
ariaDescription: string | null;
ariaLabel: string | null;
protected basicChipAttrName: string;
// (undocumented)
Expand Down Expand Up @@ -113,7 +114,7 @@ export class MatChip extends _MatChipMixinBase implements AfterViewInit, CanColo
// (undocumented)
protected _value: any;
// (undocumented)
static ɵcmp: i0.ɵɵComponentDeclaration<MatChip, "mat-basic-chip, mat-chip", ["matChip"], { "color": "color"; "disabled": "disabled"; "disableRipple": "disableRipple"; "tabIndex": "tabIndex"; "role": "role"; "id": "id"; "ariaLabel": "aria-label"; "value": "value"; "removable": "removable"; "highlighted": "highlighted"; }, { "removed": "removed"; "destroyed": "destroyed"; }, ["leadingIcon", "trailingIcon", "removeIcon"], ["mat-chip-avatar, [matChipAvatar]", "*", "mat-chip-trailing-icon,[matChipRemove],[matChipTrailingIcon]"], false, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MatChip, "mat-basic-chip, mat-chip", ["matChip"], { "color": "color"; "disabled": "disabled"; "disableRipple": "disableRipple"; "tabIndex": "tabIndex"; "role": "role"; "id": "id"; "ariaLabel": "aria-label"; "ariaDescription": "aria-description"; "value": "value"; "removable": "removable"; "highlighted": "highlighted"; }, { "removed": "removed"; "destroyed": "destroyed"; }, ["leadingIcon", "trailingIcon", "removeIcon"], ["mat-chip-avatar, [matChipAvatar]", "*", "mat-chip-trailing-icon,[matChipRemove],[matChipTrailingIcon]"], false, never>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<MatChip, [null, null, null, null, null, { optional: true; }, { optional: true; }, { attribute: "tabindex"; }]>;
}
Expand Down