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
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