Skip to content

Commit 4926cc5

Browse files
committed
fix(material/chips): allow focusing disabled listbox options (#25771)
Allow user to focus to disabled listbox options. Unlike other chips, disabled `MatChipOption` remains in the tab order, but it cannot be clicked. This aligns with WAI ARIA documented best practice to allow focusing disabled listbox options. Fix issue where screen reader does not announce disabled item in single selection list. Summary of API and behavior changes: - when disabled, `MatChipOption` sets `aria-disabled="true"` and omits `disabled` attribute. - Add private `@Input _alowFocusWithDisabled` to `MatChipAction` to support focusing the action when it is disabled. - `MatChipSet` defines `_skipPredicate` as an instance member which dervied classes may override. `_alowFocusWithDisabled` and `_skipPredicate` are internal API to the chips. (cherry picked from commit 3f68996)
1 parent 8ba7246 commit 4926cc5

File tree

7 files changed

+63
-8
lines changed

7 files changed

+63
-8
lines changed

src/dev-app/chips/chips-demo.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ <h4>Single selection</h4>
108108
<mat-chip-listbox multiple="false" [disabled]="disabledListboxes">
109109
<mat-chip-option>Extra Small</mat-chip-option>
110110
<mat-chip-option>Small</mat-chip-option>
111-
<mat-chip-option>Medium</mat-chip-option>
111+
<mat-chip-option disabled>Medium</mat-chip-option>
112112
<mat-chip-option>Large</mat-chip-option>
113113
</mat-chip-listbox>
114114

src/material/chips/chip-action.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ const _MatChipActionMixinBase = mixinTabIndex(_MatChipActionBase, -1);
3232
// in order to avoid some super-specific `:hover` styles from MDC.
3333
'[class.mdc-evolution-chip__action--presentational]': '_isPrimary',
3434
'[class.mdc-evolution-chip__action--trailing]': '!_isPrimary',
35-
'[attr.tabindex]': '(disabled || !isInteractive) ? null : tabIndex',
36-
'[attr.disabled]': "disabled ? '' : null",
35+
'[attr.tabindex]': '_getTabindex()',
36+
'[attr.disabled]': '_getDisabledAttribute()',
3737
'[attr.aria-disabled]': 'disabled',
3838
'(click)': '_handleClick($event)',
3939
'(keydown)': '_handleKeydown($event)',
@@ -56,6 +56,30 @@ export class MatChipAction extends _MatChipActionMixinBase implements HasTabInde
5656
}
5757
private _disabled = false;
5858

59+
/**
60+
* Private API to allow focusing this chip when it is disabled.
61+
*/
62+
@Input()
63+
private _allowFocusWhenDisabled = false;
64+
65+
/**
66+
* Determine the value of the disabled attribute for this chip action.
67+
*/
68+
protected _getDisabledAttribute(): string | null {
69+
// When this chip action is disabled and focusing disabled chips is not permitted, return empty
70+
// string to indicate that disabled attribute should be included.
71+
return this.disabled && !this._allowFocusWhenDisabled ? '' : null;
72+
}
73+
74+
/**
75+
* Determine the value of the tabindex attribute for this chip action.
76+
*/
77+
protected _getTabindex(): string | null {
78+
return (this.disabled && !this._allowFocusWhenDisabled) || !this.isInteractive
79+
? null
80+
: this.tabIndex.toString();
81+
}
82+
5983
constructor(
6084
public _elementRef: ElementRef<HTMLElement>,
6185
@Inject(MAT_CHIP)

src/material/chips/chip-listbox.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
10+
import {MatChipAction} from './chip-action';
1011
import {TAB} from '@angular/cdk/keycodes';
1112
import {
1213
AfterContentInit,
@@ -364,4 +365,21 @@ export class MatChipListbox
364365
return this.selected;
365366
}
366367
}
368+
369+
/**
370+
* Determines if key manager should avoid putting a given chip action in the tab index. Skip
371+
* non-interactive actions since the user can't do anything with them.
372+
*/
373+
protected override _skipPredicate(action: MatChipAction): boolean {
374+
// Override the skip predicate in the base class to avoid skipping disabled chips. Allow
375+
// disabled chip options to receive focus to align with WAI ARIA recommendation. Normally WAI
376+
// ARIA's instructions are to exclude disabled items from the tab order, but it makes a few
377+
// exceptions for compound widgets.
378+
//
379+
// From [Developing a Keyboard Interface](
380+
// https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/):
381+
// "For the following composite widget elements, keep them focusable when disabled: Options in a
382+
// Listbox..."
383+
return !action.isInteractive;
384+
}
367385
}

src/material/chips/chip-option.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<button
99
matChipAction
1010
[tabIndex]="tabIndex"
11-
[disabled]="disabled"
11+
[_allowFocusWhenDisabled]="true"
1212
[attr.aria-selected]="ariaSelected"
1313
[attr.aria-label]="ariaLabel"
1414
role="option">

src/material/chips/chip-option.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ export class MatChipSelectionChange {
3232
}
3333

3434
/**
35-
* An extension of the MatChip component that supports chip selection.
36-
* Used with MatChipListbox.
35+
* An extension of the MatChip component that supports chip selection. Used with MatChipListbox.
36+
*
37+
* Unlike other chips, the user can focus on disabled chip options inside a MatChipListbox. The
38+
* user cannot click disabled chips.
3739
*/
3840
@Component({
3941
selector: 'mat-basic-chip-option, mat-chip-option',

src/material/chips/chip-set.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,7 @@ export class MatChipSet
249249
.withVerticalOrientation()
250250
.withHorizontalOrientation(this._dir ? this._dir.value : 'ltr')
251251
.withHomeAndEnd()
252-
// Skip non-interactive and disabled actions since the user can't do anything with them.
253-
.skipPredicate(action => !action.isInteractive || action.disabled);
252+
.skipPredicate(action => this._skipPredicate(action));
254253

255254
// Keep the manager active index in sync so that navigation picks
256255
// up from the current chip if the user clicks into the list directly.
@@ -267,6 +266,16 @@ export class MatChipSet
267266
.subscribe(direction => this._keyManager.withHorizontalOrientation(direction));
268267
}
269268

269+
/**
270+
* Determines if key manager should avoid putting a given chip action in the tab index. Skip
271+
* non-interactive and disabled actions since the user can't do anything with them.
272+
*/
273+
protected _skipPredicate(action: MatChipAction): boolean {
274+
// Skip chips that the user cannot interact with. `mat-chip-set` does not permit focusing disabled
275+
// chips.
276+
return !action.isInteractive || action.disabled;
277+
}
278+
270279
/** Listens to changes in the chip set and syncs up the state of the individual chips. */
271280
private _trackChipSetChanges() {
272281
this._chips.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => {

tools/public_api_guard/material/chips.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, OnDe
310310
get selected(): MatChipOption[] | MatChipOption;
311311
setDisabledState(isDisabled: boolean): void;
312312
_setSelectionByValue(value: any, isUserInput?: boolean): void;
313+
protected _skipPredicate(action: MatChipAction): boolean;
313314
get value(): any;
314315
set value(value: any);
315316
// (undocumented)
@@ -449,6 +450,7 @@ export class MatChipSet extends _MatChipSetMixinBase implements AfterViewInit, H
449450
protected _originatesFromChip(event: Event): boolean;
450451
get role(): string | null;
451452
set role(value: string | null);
453+
protected _skipPredicate(action: MatChipAction): boolean;
452454
protected _syncChipsState(): void;
453455
// (undocumented)
454456
static ɵcmp: i0.ɵɵComponentDeclaration<MatChipSet, "mat-chip-set", never, { "disabled": "disabled"; "role": "role"; }, {}, ["_chips"], ["*"], false, never>;

0 commit comments

Comments
 (0)