From 8c12eb41cee12b0c48ac8c343fc131ae406ec671 Mon Sep 17 00:00:00 2001 From: Zach Arend Date: Mon, 7 Nov 2022 22:04:56 +0000 Subject: [PATCH] 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. --- .../chips-input/chips-input-example.html | 5 ++-- .../chips/chips-input/chips-input-example.ts | 2 +- src/material/chips/chip-option.html | 1 + src/material/chips/chip-option.spec.ts | 22 ++++++++++++++++- src/material/chips/chip-option.ts | 1 + src/material/chips/chip-row.html | 3 ++- src/material/chips/chip-row.spec.ts | 24 ++++++++++++++++++- src/material/chips/chip-row.ts | 1 + src/material/chips/chip.ts | 3 +++ tools/public_api_guard/material/chips.md | 3 ++- 10 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/components-examples/material/chips/chips-input/chips-input-example.html b/src/components-examples/material/chips/chips-input/chips-input-example.html index ba2d4f3c7be1..37e221c4500c 100644 --- a/src/components-examples/material/chips/chips-input/chips-input-example.html +++ b/src/components-examples/material/chips/chips-input/chips-input-example.html @@ -4,9 +4,10 @@ + (edited)="edit(fruit, $event)" + [aria-description]="'press enter to edit ' + fruit.name"> {{fruit.name}} - diff --git a/src/components-examples/material/chips/chips-input/chips-input-example.ts b/src/components-examples/material/chips/chips-input/chips-input-example.ts index 38acbfb16d72..346b23d5468d 100644 --- a/src/components-examples/material/chips/chips-input/chips-input-example.ts +++ b/src/components-examples/material/chips/chips-input/chips-input-example.ts @@ -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; } } diff --git a/src/material/chips/chip-option.html b/src/material/chips/chip-option.html index 3ea7c065b369..6419eeb6a294 100644 --- a/src/material/chips/chip-option.html +++ b/src/material/chips/chip-option.html @@ -11,6 +11,7 @@ [_allowFocusWhenDisabled]="true" [attr.aria-selected]="ariaSelected" [attr.aria-label]="ariaLabel" + [attr.aria-description]="ariaDescription" role="option"> diff --git a/src/material/chips/chip-option.spec.ts b/src/material/chips/chip-option.spec.ts index fed7f7eb4213..4ee1dd1ecbb3 100644 --- a/src/material/chips/chip-option.spec.ts +++ b/src/material/chips/chip-option.spec.ts @@ -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(); @@ -310,7 +327,8 @@ describe('MDC-based Option Chips', () => { + (selectionChange)="chipSelectionChange($event)" + [aria-label]="ariaLabel" [aria-description]="ariaDescription"> {{name}} @@ -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 = () => {}; diff --git a/src/material/chips/chip-option.ts b/src/material/chips/chip-option.ts index 66d9ec3b02b5..7b9bc4c13642 100644 --- a/src/material/chips/chip-option.ts +++ b/src/material/chips/chip-option.ts @@ -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', }, diff --git a/src/material/chips/chip-row.html b/src/material/chips/chip-row.html index bf7473656c66..ea013feaffa1 100644 --- a/src/material/chips/chip-row.html +++ b/src/material/chips/chip-row.html @@ -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"> diff --git a/src/material/chips/chip-row.spec.ts b/src/material/chips/chip-row.spec.ts index 72d3f4967cf8..20b198679ba3 100644 --- a/src/material/chips/chip-row.spec.ts +++ b/src/material/chips/chip-row.spec.ts @@ -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'); + }); + }); }); }); @@ -342,7 +361,8 @@ describe('MDC-based Row Chips', () => { + (removed)="chipRemove($event)" (edited)="chipEdit($event)" + [aria-label]="ariaLabel" [aria-description]="ariaDescription"> {{name}} @@ -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 = () => {}; diff --git a/src/material/chips/chip-row.ts b/src/material/chips/chip-row.ts index a648c7c657a0..aef258f0a565 100644 --- a/src/material/chips/chip-row.ts +++ b/src/material/chips/chip-row.ts @@ -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)', diff --git a/src/material/chips/chip.ts b/src/material/chips/chip.ts index e8b6f8c4233c..66e975061eb5 100644 --- a/src/material/chips/chip.ts +++ b/src/material/chips/chip.ts @@ -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; /** diff --git a/tools/public_api_guard/material/chips.md b/tools/public_api_guard/material/chips.md index b67953fb9d6e..03eb3efc4efb 100644 --- a/tools/public_api_guard/material/chips.md +++ b/tools/public_api_guard/material/chips.md @@ -65,6 +65,7 @@ export const MAT_CHIPS_DEFAULT_OPTIONS: InjectionToken; export class MatChip extends _MatChipMixinBase implements AfterViewInit, CanColor, CanDisableRipple, CanDisable, HasTabIndex, OnDestroy { constructor(_changeDetectorRef: ChangeDetectorRef, elementRef: ElementRef, _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) @@ -113,7 +114,7 @@ export class MatChip extends _MatChipMixinBase implements AfterViewInit, CanColo // (undocumented) protected _value: any; // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; }