Skip to content

Commit

Permalink
fix(material/chips): add opt-out for single-select checkmarks (#26338)
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
zarend authored Jan 6, 2023
1 parent 75ee27d commit 5e96eb0
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 14 deletions.
30 changes: 22 additions & 8 deletions src/dev-app/chips/chips-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -102,23 +102,26 @@ <h4>With Events</h4>
<button mat-button (click)="disabledListboxes = !disabledListboxes">
{{disabledListboxes ? "Enable" : "Disable"}}
</button>
<button mat-button (click)="listboxesWithAvatar = !listboxesWithAvatar">
{{listboxesWithAvatar ? "Hide Avatar" : "Show Avatar"}}
</button>

<h4>Single selection</h4>

<mat-chip-listbox multiple="false" [disabled]="disabledListboxes">
<mat-chip-option>Extra Small</mat-chip-option>
<mat-chip-option>Small</mat-chip-option>
<mat-chip-option disabled>Medium</mat-chip-option>
<mat-chip-option>Large</mat-chip-option>
<mat-chip-option *ngFor="let shirtSize of shirtSizes" [disabled]="shirtSize.disabled">
{{shirtSize.label}}
<mat-chip-avatar *ngIf="listboxesWithAvatar">{{shirtSize.avatar}}</mat-chip-avatar>
</mat-chip-option>
</mat-chip-listbox>

<h4>Multi selection</h4>

<mat-chip-listbox multiple="true" [disabled]="disabledListboxes">
<mat-chip-option selected="true">Open Now</mat-chip-option>
<mat-chip-option>Takes Reservations</mat-chip-option>
<mat-chip-option selected="true">Pet Friendly</mat-chip-option>
<mat-chip-option>Good for Brunch</mat-chip-option>
<mat-chip-option *ngFor="let hint of restaurantHints" [selected]="hint.selected">
<mat-chip-avatar *ngIf="listboxesWithAvatar">{{hint.avatar}}</mat-chip-avatar>
{{hint.label}}
</mat-chip-option>
</mat-chip-listbox>

</mat-card-content>
Expand Down Expand Up @@ -234,6 +237,17 @@ <h4>NgModel with single selection</h4>
</mat-chip-listbox>

The selected color is {{selectedColor}}.

<h4>Single selection without checkmark indicator.</h4>

<mat-chip-listbox [(ngModel)]="selectedColor" [hideSingleSelectionIndicator]="true">
<mat-chip-option *ngFor="let aColor of availableColors" [color]="aColor.color"
[value]="aColor.name">
{{aColor.name}}
</mat-chip-option>
</mat-chip-listbox>

The selected color is {{selectedColor}}.
</mat-card-content>
</mat-card>
</div>
15 changes: 15 additions & 0 deletions src/dev-app/chips/chips-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,25 @@ export class ChipsDemo {
removable = true;
addOnBlur = true;
disabledListboxes = false;
listboxesWithAvatar = false;
disableInputs = false;
editable = false;
message = '';

shirtSizes = [
{label: 'Extra Small', avatar: 'XS', disabled: false},
{label: 'Small', avatar: 'S', disabled: false},
{label: 'Medium', avatar: 'M', disabled: true},
{label: 'Large', avatar: 'L', disabled: false},
];

restaurantHints = [
{label: 'Open Now', avatar: 'O', selected: true},
{label: 'Takes Reservations', avatar: 'R', selected: false},
{label: 'Pet Friendly', avatar: 'P', selected: true},
{label: 'Good for Brunch', avatar: 'B', selected: false},
];

// Enter, comma, semi-colon
separatorKeysCodes = [ENTER, COMMA, 186];

Expand Down
18 changes: 18 additions & 0 deletions src/material/chips/chip-listbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
ContentChildren,
EventEmitter,
forwardRef,
inject,
Input,
OnDestroy,
Output,
Expand All @@ -28,6 +29,7 @@ import {startWith, takeUntil} from 'rxjs/operators';
import {MatChip, MatChipEvent} from './chip';
import {MatChipOption, MatChipSelectionChange} from './chip-option';
import {MatChipSet} from './chip-set';
import {MAT_CHIPS_DEFAULT_OPTIONS} from './tokens';

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

/** Default chip options. */
private _defaultOptions = inject(MAT_CHIPS_DEFAULT_OPTIONS, {optional: true});

/** Whether the user should be allowed to select multiple chips. */
@Input()
get multiple(): boolean {
Expand Down Expand Up @@ -158,6 +163,18 @@ export class MatChipListbox
}
protected _required: boolean = false;

/** Whether checkmark indicator for single-selection options is hidden. */
@Input()
get hideSingleSelectionIndicator(): boolean {
return this._hideSingleSelectionIndicator;
}
set hideSingleSelectionIndicator(value: BooleanInput) {
this._hideSingleSelectionIndicator = coerceBooleanProperty(value);
this._syncListboxProperties();
}
private _hideSingleSelectionIndicator: boolean =
this._defaultOptions?.hideSingleSelectionIndicator ?? false;

/** Combined stream of all of the child chips' selection change events. */
get chipSelectionChanges(): Observable<MatChipSelectionChange> {
return this._getChipStream<MatChipSelectionChange, MatChipOption>(chip => chip.selectionChange);
Expand Down Expand Up @@ -363,6 +380,7 @@ export class MatChipListbox
this._chips.forEach(chip => {
chip._chipListMultiple = this.multiple;
chip.chipListSelectable = this._selectable;
chip._chipListHideSingleSelectionIndicator = this.hideSingleSelectionIndicator;
chip._changeDetectorRef.markForCheck();
});
});
Expand Down
2 changes: 1 addition & 1 deletion src/material/chips/chip-option.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
[attr.aria-label]="ariaLabel"
[attr.aria-describedby]="_ariaDescriptionId"
role="option">
<span class="mdc-evolution-chip__graphic mat-mdc-chip-graphic">
<span class="mdc-evolution-chip__graphic mat-mdc-chip-graphic" *ngIf="_hasLeadingGraphic()">
<ng-content select="mat-chip-avatar, [matChipAvatar]"></ng-content>
<span class="mdc-evolution-chip__checkmark">
<svg class="mdc-evolution-chip__checkmark-svg" viewBox="-2 -3 30 30" focusable="false">
Expand Down
60 changes: 58 additions & 2 deletions src/material/chips/chip-option.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import {
MatChipEvent,
MatChipListbox,
MatChipOption,
MatChipsDefaultOptions,
MatChipSelectionChange,
MatChipsModule,
MAT_CHIPS_DEFAULT_OPTIONS,
} from './index';
import {SPACE} from '@angular/cdk/keycodes';
import {ENTER, SPACE} from '@angular/cdk/keycodes';

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

let hideSingleSelectionIndicator: boolean | undefined;

beforeEach(waitForAsync(() => {
globalRippleOptions = {};
const defaultOptions: MatChipsDefaultOptions = {
separatorKeyCodes: [ENTER, SPACE],
hideSingleSelectionIndicator,
};

TestBed.configureTestingModule({
imports: [MatChipsModule],
declarations: [SingleChip],
Expand All @@ -37,6 +46,7 @@ describe('MDC-based Option Chips', () => {
change: new Subject(),
}),
},
{provide: MAT_CHIPS_DEFAULT_OPTIONS, useFactory: () => defaultOptions},
],
});

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

expect(primaryAction.getAttribute('aria-disabled')).toBe('true');
});

it('should display checkmark graphic by default', () => {
expect(
fixture.debugElement.injector.get(MAT_CHIPS_DEFAULT_OPTIONS)
?.hideSingleSelectionIndicator,
)
.withContext(
'expected not to have a default value set for `hideSingleSelectionIndicator`',
)
.toBeUndefined();

expect(chipNativeElement.querySelector('.mat-mdc-chip-graphic')).toBeTruthy();
expect(chipNativeElement.classList).toContain('mdc-evolution-chip--with-primary-graphic');
});
});

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

expect(optionElementDescription).toMatch(/option description/i);
});

it('should display checkmark graphic by default', () => {
expect(chipNativeElement.querySelector('.mat-mdc-chip-graphic')).toBeTruthy();
expect(chipNativeElement.classList).toContain('mdc-evolution-chip--with-primary-graphic');
});
});

describe('with token to hide single-selection checkmark indicator', () => {
beforeAll(() => {
hideSingleSelectionIndicator = true;
});

afterAll(() => {
hideSingleSelectionIndicator = undefined;
});

it('does not display checkmark graphic', () => {
expect(chipNativeElement.querySelector('.mat-mdc-chip-graphic')).toBeNull();
expect(chipNativeElement.classList).not.toContain(
'mdc-evolution-chip--with-primary-graphic',
);
});

it('displays checkmark graphic when avatar is provided', () => {
testComponent.selected = true;
testComponent.avatarLabel = 'A';
fixture.detectChanges();

expect(chipNativeElement.querySelector('.mat-mdc-chip-graphic')).toBeTruthy();
expect(chipNativeElement.classList).toContain('mdc-evolution-chip--with-primary-graphic');
});
});

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

chipDestroy: (event?: MatChipEvent) => void = () => {};
chipSelectionChange: (event?: MatChipSelectionChange) => void = () => {};
Expand Down
24 changes: 22 additions & 2 deletions src/material/chips/chip-option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import {
Output,
ViewEncapsulation,
OnInit,
inject,
} from '@angular/core';
import {MatChip} from './chip';
import {MAT_CHIP} from './tokens';
import {MAT_CHIP, MAT_CHIPS_DEFAULT_OPTIONS} from './tokens';

/** Event object emitted by MatChipOption when selected or deselected. */
export class MatChipSelectionChange {
Expand All @@ -44,7 +45,7 @@ export class MatChipSelectionChange {
inputs: ['color', 'disabled', 'disableRipple', 'tabIndex'],
host: {
'class':
'mat-mdc-chip mat-mdc-chip-option mdc-evolution-chip mdc-evolution-chip--filter mdc-evolution-chip--selectable mdc-evolution-chip--with-primary-graphic',
'mat-mdc-chip mat-mdc-chip-option mdc-evolution-chip mdc-evolution-chip--filter mdc-evolution-chip--selectable',
'[class.mat-mdc-chip-selected]': 'selected',
'[class.mat-mdc-chip-multiple]': '_chipListMultiple',
'[class.mat-mdc-chip-disabled]': 'disabled',
Expand All @@ -58,6 +59,7 @@ export class MatChipSelectionChange {
'[class.mdc-evolution-chip--selecting]': '!_animationsDisabled',
'[class.mdc-evolution-chip--with-trailing-action]': '_hasTrailingIcon()',
'[class.mdc-evolution-chip--with-primary-icon]': 'leadingIcon',
'[class.mdc-evolution-chip--with-primary-graphic]': '_hasLeadingGraphic()',
'[class.mdc-evolution-chip--with-avatar]': 'leadingIcon',
'[class.mat-mdc-chip-highlighted]': 'highlighted',
'[class.mat-mdc-chip-with-trailing-icon]': '_hasTrailingIcon()',
Expand All @@ -75,12 +77,19 @@ export class MatChipSelectionChange {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MatChipOption extends MatChip implements OnInit {
/** Default chip options. */
private _defaultOptions = inject(MAT_CHIPS_DEFAULT_OPTIONS, {optional: true});

/** Whether the chip list is selectable. */
chipListSelectable: boolean = true;

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

/** Whether the chip list hides single-selection indicator. */
_chipListHideSingleSelectionIndicator: boolean =
this._defaultOptions?.hideSingleSelectionIndicator ?? false;

/**
* Whether or not the chip is selectable.
*
Expand Down Expand Up @@ -163,6 +172,17 @@ export class MatChipOption extends MatChip implements OnInit {
}
}

_hasLeadingGraphic() {
if (this.leadingIcon) {
return true;
}

// The checkmark graphic communicates selected state for both single-select and multi-select.
// Include checkmark in single-select to fix a11y issue where selected state is communicated
// visually only using color (#25886).
return !this._chipListHideSingleSelectionIndicator || this._chipListMultiple;
}

_setSelectedState(isSelected: boolean, isUserInput: boolean, emitEvent: boolean) {
if (isSelected !== this.selected) {
this._selected = isSelected;
Expand Down
3 changes: 3 additions & 0 deletions src/material/chips/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import {InjectionToken} from '@angular/core';
export interface MatChipsDefaultOptions {
/** The list of key codes that will trigger a chipEnd event. */
separatorKeyCodes: readonly number[] | ReadonlySet<number>;

/** Wheter icon indicators should be hidden for single-selection. */
hideSingleSelectionIndicator?: boolean;
}

/** Injection token to be used to override the default options for the chips module. */
Expand Down
8 changes: 7 additions & 1 deletion tools/public_api_guard/material/chips.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,8 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, OnDe
// (undocumented)
protected _defaultRole: string;
focus(): void;
get hideSingleSelectionIndicator(): boolean;
set hideSingleSelectionIndicator(value: BooleanInput);
// (undocumented)
_keydown(event: KeyboardEvent): void;
get multiple(): boolean;
Expand Down Expand Up @@ -317,7 +319,7 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, OnDe
protected _value: any;
writeValue(value: any): void;
// (undocumented)
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>;
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>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<MatChipListbox, never>;
}
Expand All @@ -335,12 +337,15 @@ export class MatChipListboxChange {
export class MatChipOption extends MatChip implements OnInit {
get ariaSelected(): string | null;
protected basicChipAttrName: string;
_chipListHideSingleSelectionIndicator: boolean;
_chipListMultiple: boolean;
chipListSelectable: boolean;
deselect(): void;
// (undocumented)
_handlePrimaryActionInteraction(): void;
// (undocumented)
_hasLeadingGraphic(): boolean;
// (undocumented)
ngOnInit(): void;
select(): void;
get selectable(): boolean;
Expand Down Expand Up @@ -401,6 +406,7 @@ export class MatChipRow extends MatChip implements AfterViewInit {

// @public
export interface MatChipsDefaultOptions {
hideSingleSelectionIndicator?: boolean;
separatorKeyCodes: readonly number[] | ReadonlySet<number>;
}

Expand Down

0 comments on commit 5e96eb0

Please sign in to comment.