Skip to content

Commit 5e96eb0

Browse files
authored
fix(material/chips): add opt-out for single-select checkmarks (#26338)
Add an opt-out for checkmark indicators for single-selection. Add both and Input and DI token to specify if checkmark indicators are hidden for single-select. By default display checkmark indicators for single-selection. If both DI token and Input are specified, the Input wins. PR #25890 adds checkmork indicator for single selection. Add an opt-out to provide a way to have same appearance as before #25890. Does not affect multiple-selection. Does not affect behavior when avatar is provided. When avatar is provided, display checkmark indicator when selected. This is the same behavior as before #25890. API Changes - Add `@Input hideSingleSelectionIndicator` to specify if checkmark indicator is displayed for single-selection - Add `hideSingleSelectionIndicator` property to `MatChipsDefaultOptions`, which specifies default value for `hideSingleSelectionIndicator`.
1 parent 75ee27d commit 5e96eb0

File tree

8 files changed

+146
-14
lines changed

8 files changed

+146
-14
lines changed

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

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,23 +102,26 @@ <h4>With Events</h4>
102102
<button mat-button (click)="disabledListboxes = !disabledListboxes">
103103
{{disabledListboxes ? "Enable" : "Disable"}}
104104
</button>
105+
<button mat-button (click)="listboxesWithAvatar = !listboxesWithAvatar">
106+
{{listboxesWithAvatar ? "Hide Avatar" : "Show Avatar"}}
107+
</button>
105108

106109
<h4>Single selection</h4>
107110

108111
<mat-chip-listbox multiple="false" [disabled]="disabledListboxes">
109-
<mat-chip-option>Extra Small</mat-chip-option>
110-
<mat-chip-option>Small</mat-chip-option>
111-
<mat-chip-option disabled>Medium</mat-chip-option>
112-
<mat-chip-option>Large</mat-chip-option>
112+
<mat-chip-option *ngFor="let shirtSize of shirtSizes" [disabled]="shirtSize.disabled">
113+
{{shirtSize.label}}
114+
<mat-chip-avatar *ngIf="listboxesWithAvatar">{{shirtSize.avatar}}</mat-chip-avatar>
115+
</mat-chip-option>
113116
</mat-chip-listbox>
114117

115118
<h4>Multi selection</h4>
116119

117120
<mat-chip-listbox multiple="true" [disabled]="disabledListboxes">
118-
<mat-chip-option selected="true">Open Now</mat-chip-option>
119-
<mat-chip-option>Takes Reservations</mat-chip-option>
120-
<mat-chip-option selected="true">Pet Friendly</mat-chip-option>
121-
<mat-chip-option>Good for Brunch</mat-chip-option>
121+
<mat-chip-option *ngFor="let hint of restaurantHints" [selected]="hint.selected">
122+
<mat-chip-avatar *ngIf="listboxesWithAvatar">{{hint.avatar}}</mat-chip-avatar>
123+
{{hint.label}}
124+
</mat-chip-option>
122125
</mat-chip-listbox>
123126

124127
</mat-card-content>
@@ -234,6 +237,17 @@ <h4>NgModel with single selection</h4>
234237
</mat-chip-listbox>
235238

236239
The selected color is {{selectedColor}}.
240+
241+
<h4>Single selection without checkmark indicator.</h4>
242+
243+
<mat-chip-listbox [(ngModel)]="selectedColor" [hideSingleSelectionIndicator]="true">
244+
<mat-chip-option *ngFor="let aColor of availableColors" [color]="aColor.color"
245+
[value]="aColor.name">
246+
{{aColor.name}}
247+
</mat-chip-option>
248+
</mat-chip-listbox>
249+
250+
The selected color is {{selectedColor}}.
237251
</mat-card-content>
238252
</mat-card>
239253
</div>

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,25 @@ export class ChipsDemo {
5252
removable = true;
5353
addOnBlur = true;
5454
disabledListboxes = false;
55+
listboxesWithAvatar = false;
5556
disableInputs = false;
5657
editable = false;
5758
message = '';
5859

60+
shirtSizes = [
61+
{label: 'Extra Small', avatar: 'XS', disabled: false},
62+
{label: 'Small', avatar: 'S', disabled: false},
63+
{label: 'Medium', avatar: 'M', disabled: true},
64+
{label: 'Large', avatar: 'L', disabled: false},
65+
];
66+
67+
restaurantHints = [
68+
{label: 'Open Now', avatar: 'O', selected: true},
69+
{label: 'Takes Reservations', avatar: 'R', selected: false},
70+
{label: 'Pet Friendly', avatar: 'P', selected: true},
71+
{label: 'Good for Brunch', avatar: 'B', selected: false},
72+
];
73+
5974
// Enter, comma, semi-colon
6075
separatorKeysCodes = [ENTER, COMMA, 186];
6176

src/material/chips/chip-listbox.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
ContentChildren,
1717
EventEmitter,
1818
forwardRef,
19+
inject,
1920
Input,
2021
OnDestroy,
2122
Output,
@@ -28,6 +29,7 @@ import {startWith, takeUntil} from 'rxjs/operators';
2829
import {MatChip, MatChipEvent} from './chip';
2930
import {MatChipOption, MatChipSelectionChange} from './chip-option';
3031
import {MatChipSet} from './chip-set';
32+
import {MAT_CHIPS_DEFAULT_OPTIONS} from './tokens';
3133

3234
/** Change event object that is emitted when the chip listbox value has changed. */
3335
export class MatChipListboxChange {
@@ -105,6 +107,9 @@ export class MatChipListbox
105107
/** Value that was assigned before the listbox was initialized. */
106108
private _pendingInitialValue: any;
107109

110+
/** Default chip options. */
111+
private _defaultOptions = inject(MAT_CHIPS_DEFAULT_OPTIONS, {optional: true});
112+
108113
/** Whether the user should be allowed to select multiple chips. */
109114
@Input()
110115
get multiple(): boolean {
@@ -158,6 +163,18 @@ export class MatChipListbox
158163
}
159164
protected _required: boolean = false;
160165

166+
/** Whether checkmark indicator for single-selection options is hidden. */
167+
@Input()
168+
get hideSingleSelectionIndicator(): boolean {
169+
return this._hideSingleSelectionIndicator;
170+
}
171+
set hideSingleSelectionIndicator(value: BooleanInput) {
172+
this._hideSingleSelectionIndicator = coerceBooleanProperty(value);
173+
this._syncListboxProperties();
174+
}
175+
private _hideSingleSelectionIndicator: boolean =
176+
this._defaultOptions?.hideSingleSelectionIndicator ?? false;
177+
161178
/** Combined stream of all of the child chips' selection change events. */
162179
get chipSelectionChanges(): Observable<MatChipSelectionChange> {
163180
return this._getChipStream<MatChipSelectionChange, MatChipOption>(chip => chip.selectionChange);
@@ -363,6 +380,7 @@ export class MatChipListbox
363380
this._chips.forEach(chip => {
364381
chip._chipListMultiple = this.multiple;
365382
chip.chipListSelectable = this._selectable;
383+
chip._chipListHideSingleSelectionIndicator = this.hideSingleSelectionIndicator;
366384
chip._changeDetectorRef.markForCheck();
367385
});
368386
});

src/material/chips/chip-option.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
[attr.aria-label]="ariaLabel"
1414
[attr.aria-describedby]="_ariaDescriptionId"
1515
role="option">
16-
<span class="mdc-evolution-chip__graphic mat-mdc-chip-graphic">
16+
<span class="mdc-evolution-chip__graphic mat-mdc-chip-graphic" *ngIf="_hasLeadingGraphic()">
1717
<ng-content select="mat-chip-avatar, [matChipAvatar]"></ng-content>
1818
<span class="mdc-evolution-chip__checkmark">
1919
<svg class="mdc-evolution-chip__checkmark-svg" viewBox="-2 -3 30 30" focusable="false">

src/material/chips/chip-option.spec.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import {
99
MatChipEvent,
1010
MatChipListbox,
1111
MatChipOption,
12+
MatChipsDefaultOptions,
1213
MatChipSelectionChange,
1314
MatChipsModule,
15+
MAT_CHIPS_DEFAULT_OPTIONS,
1416
} from './index';
15-
import {SPACE} from '@angular/cdk/keycodes';
17+
import {ENTER, SPACE} from '@angular/cdk/keycodes';
1618

1719
describe('MDC-based Option Chips', () => {
1820
let fixture: ComponentFixture<any>;
@@ -23,8 +25,15 @@ describe('MDC-based Option Chips', () => {
2325
let globalRippleOptions: RippleGlobalOptions;
2426
let dir = 'ltr';
2527

28+
let hideSingleSelectionIndicator: boolean | undefined;
29+
2630
beforeEach(waitForAsync(() => {
2731
globalRippleOptions = {};
32+
const defaultOptions: MatChipsDefaultOptions = {
33+
separatorKeyCodes: [ENTER, SPACE],
34+
hideSingleSelectionIndicator,
35+
};
36+
2837
TestBed.configureTestingModule({
2938
imports: [MatChipsModule],
3039
declarations: [SingleChip],
@@ -37,6 +46,7 @@ describe('MDC-based Option Chips', () => {
3746
change: new Subject(),
3847
}),
3948
},
49+
{provide: MAT_CHIPS_DEFAULT_OPTIONS, useFactory: () => defaultOptions},
4050
],
4151
});
4252

@@ -294,6 +304,20 @@ describe('MDC-based Option Chips', () => {
294304

295305
expect(primaryAction.getAttribute('aria-disabled')).toBe('true');
296306
});
307+
308+
it('should display checkmark graphic by default', () => {
309+
expect(
310+
fixture.debugElement.injector.get(MAT_CHIPS_DEFAULT_OPTIONS)
311+
?.hideSingleSelectionIndicator,
312+
)
313+
.withContext(
314+
'expected not to have a default value set for `hideSingleSelectionIndicator`',
315+
)
316+
.toBeUndefined();
317+
318+
expect(chipNativeElement.querySelector('.mat-mdc-chip-graphic')).toBeTruthy();
319+
expect(chipNativeElement.classList).toContain('mdc-evolution-chip--with-primary-graphic');
320+
});
297321
});
298322

299323
describe('a11y', () => {
@@ -331,6 +355,37 @@ describe('MDC-based Option Chips', () => {
331355

332356
expect(optionElementDescription).toMatch(/option description/i);
333357
});
358+
359+
it('should display checkmark graphic by default', () => {
360+
expect(chipNativeElement.querySelector('.mat-mdc-chip-graphic')).toBeTruthy();
361+
expect(chipNativeElement.classList).toContain('mdc-evolution-chip--with-primary-graphic');
362+
});
363+
});
364+
365+
describe('with token to hide single-selection checkmark indicator', () => {
366+
beforeAll(() => {
367+
hideSingleSelectionIndicator = true;
368+
});
369+
370+
afterAll(() => {
371+
hideSingleSelectionIndicator = undefined;
372+
});
373+
374+
it('does not display checkmark graphic', () => {
375+
expect(chipNativeElement.querySelector('.mat-mdc-chip-graphic')).toBeNull();
376+
expect(chipNativeElement.classList).not.toContain(
377+
'mdc-evolution-chip--with-primary-graphic',
378+
);
379+
});
380+
381+
it('displays checkmark graphic when avatar is provided', () => {
382+
testComponent.selected = true;
383+
testComponent.avatarLabel = 'A';
384+
fixture.detectChanges();
385+
386+
expect(chipNativeElement.querySelector('.mat-mdc-chip-graphic')).toBeTruthy();
387+
expect(chipNativeElement.classList).toContain('mdc-evolution-chip--with-primary-graphic');
388+
});
334389
});
335390

336391
it('should contain a focus indicator inside the text label', () => {
@@ -349,7 +404,7 @@ describe('MDC-based Option Chips', () => {
349404
(destroyed)="chipDestroy($event)"
350405
(selectionChange)="chipSelectionChange($event)"
351406
[aria-label]="ariaLabel" [aria-description]="ariaDescription">
352-
<span class="avatar" matChipAvatar></span>
407+
<span class="avatar" matChipAvatar *ngIf="avatarLabel">{{avatarLabel}}</span>
353408
{{name}}
354409
</mat-chip-option>
355410
</div>
@@ -365,6 +420,7 @@ class SingleChip {
365420
shouldShow: boolean = true;
366421
ariaLabel: string | null = null;
367422
ariaDescription: string | null = null;
423+
avatarLabel: string | null = null;
368424

369425
chipDestroy: (event?: MatChipEvent) => void = () => {};
370426
chipSelectionChange: (event?: MatChipSelectionChange) => void = () => {};

src/material/chips/chip-option.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ import {
1515
Output,
1616
ViewEncapsulation,
1717
OnInit,
18+
inject,
1819
} from '@angular/core';
1920
import {MatChip} from './chip';
20-
import {MAT_CHIP} from './tokens';
21+
import {MAT_CHIP, MAT_CHIPS_DEFAULT_OPTIONS} from './tokens';
2122

2223
/** Event object emitted by MatChipOption when selected or deselected. */
2324
export class MatChipSelectionChange {
@@ -44,7 +45,7 @@ export class MatChipSelectionChange {
4445
inputs: ['color', 'disabled', 'disableRipple', 'tabIndex'],
4546
host: {
4647
'class':
47-
'mat-mdc-chip mat-mdc-chip-option mdc-evolution-chip mdc-evolution-chip--filter mdc-evolution-chip--selectable mdc-evolution-chip--with-primary-graphic',
48+
'mat-mdc-chip mat-mdc-chip-option mdc-evolution-chip mdc-evolution-chip--filter mdc-evolution-chip--selectable',
4849
'[class.mat-mdc-chip-selected]': 'selected',
4950
'[class.mat-mdc-chip-multiple]': '_chipListMultiple',
5051
'[class.mat-mdc-chip-disabled]': 'disabled',
@@ -58,6 +59,7 @@ export class MatChipSelectionChange {
5859
'[class.mdc-evolution-chip--selecting]': '!_animationsDisabled',
5960
'[class.mdc-evolution-chip--with-trailing-action]': '_hasTrailingIcon()',
6061
'[class.mdc-evolution-chip--with-primary-icon]': 'leadingIcon',
62+
'[class.mdc-evolution-chip--with-primary-graphic]': '_hasLeadingGraphic()',
6163
'[class.mdc-evolution-chip--with-avatar]': 'leadingIcon',
6264
'[class.mat-mdc-chip-highlighted]': 'highlighted',
6365
'[class.mat-mdc-chip-with-trailing-icon]': '_hasTrailingIcon()',
@@ -75,12 +77,19 @@ export class MatChipSelectionChange {
7577
changeDetection: ChangeDetectionStrategy.OnPush,
7678
})
7779
export class MatChipOption extends MatChip implements OnInit {
80+
/** Default chip options. */
81+
private _defaultOptions = inject(MAT_CHIPS_DEFAULT_OPTIONS, {optional: true});
82+
7883
/** Whether the chip list is selectable. */
7984
chipListSelectable: boolean = true;
8085

8186
/** Whether the chip list is in multi-selection mode. */
8287
_chipListMultiple: boolean = false;
8388

89+
/** Whether the chip list hides single-selection indicator. */
90+
_chipListHideSingleSelectionIndicator: boolean =
91+
this._defaultOptions?.hideSingleSelectionIndicator ?? false;
92+
8493
/**
8594
* Whether or not the chip is selectable.
8695
*
@@ -163,6 +172,17 @@ export class MatChipOption extends MatChip implements OnInit {
163172
}
164173
}
165174

175+
_hasLeadingGraphic() {
176+
if (this.leadingIcon) {
177+
return true;
178+
}
179+
180+
// The checkmark graphic communicates selected state for both single-select and multi-select.
181+
// Include checkmark in single-select to fix a11y issue where selected state is communicated
182+
// visually only using color (#25886).
183+
return !this._chipListHideSingleSelectionIndicator || this._chipListMultiple;
184+
}
185+
166186
_setSelectedState(isSelected: boolean, isUserInput: boolean, emitEvent: boolean) {
167187
if (isSelected !== this.selected) {
168188
this._selected = isSelected;

src/material/chips/tokens.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import {InjectionToken} from '@angular/core';
1212
export interface MatChipsDefaultOptions {
1313
/** The list of key codes that will trigger a chipEnd event. */
1414
separatorKeyCodes: readonly number[] | ReadonlySet<number>;
15+
16+
/** Wheter icon indicators should be hidden for single-selection. */
17+
hideSingleSelectionIndicator?: boolean;
1518
}
1619

1720
/** Injection token to be used to override the default options for the chips module. */

tools/public_api_guard/material/chips.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,8 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, OnDe
289289
// (undocumented)
290290
protected _defaultRole: string;
291291
focus(): void;
292+
get hideSingleSelectionIndicator(): boolean;
293+
set hideSingleSelectionIndicator(value: BooleanInput);
292294
// (undocumented)
293295
_keydown(event: KeyboardEvent): void;
294296
get multiple(): boolean;
@@ -317,7 +319,7 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, OnDe
317319
protected _value: any;
318320
writeValue(value: any): void;
319321
// (undocumented)
320-
static ɵcmp: i0.ɵɵComponentDeclaration<MatChipListbox, "mat-chip-listbox", never, { "tabIndex": "tabIndex"; "multiple": "multiple"; "ariaOrientation": "aria-orientation"; "selectable": "selectable"; "compareWith": "compareWith"; "required": "required"; "value": "value"; }, { "change": "change"; }, ["_chips"], ["*"], false, never>;
322+
static ɵcmp: i0.ɵɵComponentDeclaration<MatChipListbox, "mat-chip-listbox", never, { "tabIndex": "tabIndex"; "multiple": "multiple"; "ariaOrientation": "aria-orientation"; "selectable": "selectable"; "compareWith": "compareWith"; "required": "required"; "hideSingleSelectionIndicator": "hideSingleSelectionIndicator"; "value": "value"; }, { "change": "change"; }, ["_chips"], ["*"], false, never>;
321323
// (undocumented)
322324
static ɵfac: i0.ɵɵFactoryDeclaration<MatChipListbox, never>;
323325
}
@@ -335,12 +337,15 @@ export class MatChipListboxChange {
335337
export class MatChipOption extends MatChip implements OnInit {
336338
get ariaSelected(): string | null;
337339
protected basicChipAttrName: string;
340+
_chipListHideSingleSelectionIndicator: boolean;
338341
_chipListMultiple: boolean;
339342
chipListSelectable: boolean;
340343
deselect(): void;
341344
// (undocumented)
342345
_handlePrimaryActionInteraction(): void;
343346
// (undocumented)
347+
_hasLeadingGraphic(): boolean;
348+
// (undocumented)
344349
ngOnInit(): void;
345350
select(): void;
346351
get selectable(): boolean;
@@ -401,6 +406,7 @@ export class MatChipRow extends MatChip implements AfterViewInit {
401406

402407
// @public
403408
export interface MatChipsDefaultOptions {
409+
hideSingleSelectionIndicator?: boolean;
404410
separatorKeyCodes: readonly number[] | ReadonlySet<number>;
405411
}
406412

0 commit comments

Comments
 (0)