Skip to content

Commit 809e1e8

Browse files
committed
feat(material/chips): add (optional) edit icon to input chips
1 parent 2fabce5 commit 809e1e8

17 files changed

+250
-24
lines changed

goldens/material/chips/index.api.md

+26-3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ export const MAT_CHIP: InjectionToken<unknown>;
3535
// @public
3636
export const MAT_CHIP_AVATAR: InjectionToken<unknown>;
3737

38+
// @public
39+
export const MAT_CHIP_EDIT: InjectionToken<unknown>;
40+
3841
// @public
3942
export const MAT_CHIP_LISTBOX_CONTROL_VALUE_ACCESSOR: any;
4043

@@ -50,6 +53,7 @@ export const MAT_CHIPS_DEFAULT_OPTIONS: InjectionToken<MatChipsDefaultOptions>;
5053
// @public
5154
export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck, OnDestroy {
5255
constructor(...args: unknown[]);
56+
protected _allEditIcons: QueryList<MatChipEdit>;
5357
protected _allLeadingIcons: QueryList<MatChipAvatar>;
5458
protected _allRemoveIcons: QueryList<MatChipRemove>;
5559
protected _allTrailingIcons: QueryList<MatChipTrailingIcon>;
@@ -68,6 +72,8 @@ export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck
6872
disableRipple: boolean;
6973
// (undocumented)
7074
protected _document: Document;
75+
_edit(event: Event): void;
76+
editIcon: MatChipEdit;
7177
// (undocumented)
7278
_elementRef: ElementRef<HTMLElement>;
7379
focus(): void;
@@ -119,7 +125,7 @@ export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck
119125
// (undocumented)
120126
protected _value: any;
121127
// (undocumented)
122-
static ɵcmp: i0.ɵɵComponentDeclaration<MatChip, "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", ["matChip"], { "role": { "alias": "role"; "required": false; }; "id": { "alias": "id"; "required": false; }; "ariaLabel": { "alias": "aria-label"; "required": false; }; "ariaDescription": { "alias": "aria-description"; "required": false; }; "value": { "alias": "value"; "required": false; }; "color": { "alias": "color"; "required": false; }; "removable": { "alias": "removable"; "required": false; }; "highlighted": { "alias": "highlighted"; "required": false; }; "disableRipple": { "alias": "disableRipple"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; }, { "removed": "removed"; "destroyed": "destroyed"; }, ["leadingIcon", "trailingIcon", "removeIcon", "_allLeadingIcons", "_allTrailingIcons", "_allRemoveIcons"], ["mat-chip-avatar, [matChipAvatar]", "*", "mat-chip-trailing-icon,[matChipRemove],[matChipTrailingIcon]"], true, never>;
128+
static ɵcmp: i0.ɵɵComponentDeclaration<MatChip, "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", ["matChip"], { "role": { "alias": "role"; "required": false; }; "id": { "alias": "id"; "required": false; }; "ariaLabel": { "alias": "aria-label"; "required": false; }; "ariaDescription": { "alias": "aria-description"; "required": false; }; "value": { "alias": "value"; "required": false; }; "color": { "alias": "color"; "required": false; }; "removable": { "alias": "removable"; "required": false; }; "highlighted": { "alias": "highlighted"; "required": false; }; "disableRipple": { "alias": "disableRipple"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; }, { "removed": "removed"; "destroyed": "destroyed"; }, ["leadingIcon", "editIcon", "trailingIcon", "removeIcon", "_allLeadingIcons", "_allTrailingIcons", "_allEditIcons", "_allRemoveIcons"], ["mat-chip-avatar, [matChipAvatar]", "*", "mat-chip-trailing-icon,[matChipRemove],[matChipTrailingIcon]"], true, never>;
123129
// (undocumented)
124130
static ɵfac: i0.ɵɵFactoryDeclaration<MatChip, never>;
125131
}
@@ -132,6 +138,20 @@ export class MatChipAvatar {
132138
static ɵfac: i0.ɵɵFactoryDeclaration<MatChipAvatar, never>;
133139
}
134140

141+
// @public
142+
export class MatChipEdit extends MatChipAction {
143+
// (undocumented)
144+
_handleClick(event: MouseEvent): void;
145+
// (undocumented)
146+
_handleKeydown(event: KeyboardEvent): void;
147+
// (undocumented)
148+
_isPrimary: boolean;
149+
// (undocumented)
150+
static ɵdir: i0.ɵɵDirectiveDeclaration<MatChipEdit, "[matChipEdit]", never, {}, {}, never, never, true, never>;
151+
// (undocumented)
152+
static ɵfac: i0.ɵɵFactoryDeclaration<MatChipEdit, never>;
153+
}
154+
135155
// @public
136156
export interface MatChipEditedEvent extends MatChipEvent {
137157
value: string;
@@ -420,21 +440,24 @@ export class MatChipRow extends MatChip implements AfterViewInit {
420440
contentEditInput?: MatChipEditInput;
421441
defaultEditInput?: MatChipEditInput;
422442
// (undocumented)
443+
_edit(): void;
444+
// (undocumented)
423445
editable: boolean;
424446
readonly edited: EventEmitter<MatChipEditedEvent>;
425447
// (undocumented)
426448
_handleDoubleclick(event: MouseEvent): void;
427449
_handleFocus(): void;
428450
// (undocumented)
429451
_handleKeydown(event: KeyboardEvent): void;
452+
_hasLeadingIcon(): boolean;
430453
// (undocumented)
431454
_hasTrailingIcon(): boolean;
432455
// (undocumented)
433456
_isEditing: boolean;
434457
// (undocumented)
435458
_isRippleDisabled(): boolean;
436459
// (undocumented)
437-
static ɵcmp: i0.ɵɵComponentDeclaration<MatChipRow, "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", never, { "editable": { "alias": "editable"; "required": false; }; }, { "edited": "edited"; }, ["contentEditInput"], ["mat-chip-avatar, [matChipAvatar]", "[matChipEditInput]", "*", "mat-chip-trailing-icon,[matChipRemove],[matChipTrailingIcon]"], true, never>;
460+
static ɵcmp: i0.ɵɵComponentDeclaration<MatChipRow, "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", never, { "editable": { "alias": "editable"; "required": false; }; }, { "edited": "edited"; }, ["contentEditInput"], ["[matChipEdit]", "mat-chip-avatar, [matChipAvatar]", "[matChipEditInput]", "*", "mat-chip-trailing-icon,[matChipRemove],[matChipTrailingIcon]"], true, never>;
438461
// (undocumented)
439462
static ɵfac: i0.ɵɵFactoryDeclaration<MatChipRow, never>;
440463
}
@@ -511,7 +534,7 @@ export class MatChipsModule {
511534
// (undocumented)
512535
static ɵinj: i0.ɵɵInjectorDeclaration<MatChipsModule>;
513536
// (undocumented)
514-
static ɵmod: i0.ɵɵNgModuleDeclaration<MatChipsModule, never, [typeof MatCommonModule, typeof MatRippleModule, typeof MatChipAction, typeof MatChip, typeof MatChipAvatar, typeof MatChipEditInput, typeof MatChipGrid, typeof MatChipInput, typeof MatChipListbox, typeof MatChipOption, typeof MatChipRemove, typeof MatChipRow, typeof MatChipSet, typeof MatChipTrailingIcon], [typeof MatCommonModule, typeof MatChip, typeof MatChipAvatar, typeof MatChipEditInput, typeof MatChipGrid, typeof MatChipInput, typeof MatChipListbox, typeof MatChipOption, typeof MatChipRemove, typeof MatChipRow, typeof MatChipSet, typeof MatChipTrailingIcon]>;
537+
static ɵmod: i0.ɵɵNgModuleDeclaration<MatChipsModule, never, [typeof MatCommonModule, typeof MatRippleModule, typeof MatChipAction, typeof MatChip, typeof MatChipAvatar, typeof MatChipEdit, typeof MatChipEditInput, typeof MatChipGrid, typeof MatChipInput, typeof MatChipListbox, typeof MatChipOption, typeof MatChipRemove, typeof MatChipRow, typeof MatChipSet, typeof MatChipTrailingIcon], [typeof MatCommonModule, typeof MatChip, typeof MatChipAvatar, typeof MatChipEdit, typeof MatChipEditInput, typeof MatChipGrid, typeof MatChipInput, typeof MatChipListbox, typeof MatChipOption, typeof MatChipRemove, typeof MatChipRow, typeof MatChipSet, typeof MatChipTrailingIcon]>;
515538
}
516539

517540
// @public

goldens/material/chips/testing/index.api.md

+13
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import { TestKey } from '@angular/cdk/testing';
1616
export interface ChipAvatarHarnessFilters extends BaseHarnessFilters {
1717
}
1818

19+
// @public (undocumented)
20+
export interface ChipEditHarnessFilters extends BaseHarnessFilters {
21+
}
22+
1923
// @public (undocumented)
2024
export interface ChipEditInputHarnessFilters extends BaseHarnessFilters {
2125
}
@@ -67,6 +71,14 @@ export class MatChipAvatarHarness extends ComponentHarness {
6771
static with<T extends MatChipAvatarHarness>(this: ComponentHarnessConstructor<T>, options?: ChipAvatarHarnessFilters): HarnessPredicate<T>;
6872
}
6973

74+
// @public
75+
export class MatChipEditHarness extends ComponentHarness {
76+
click(): Promise<void>;
77+
// (undocumented)
78+
static hostSelector: string;
79+
static with<T extends MatChipEditHarness>(this: ComponentHarnessConstructor<T>, options?: ChipEditHarnessFilters): HarnessPredicate<T>;
80+
}
81+
7082
// @public
7183
export class MatChipEditInputHarness extends ComponentHarness {
7284
// (undocumented)
@@ -89,6 +101,7 @@ export class MatChipGridHarness extends ComponentHarness {
89101

90102
// @public
91103
export class MatChipHarness extends ContentContainerComponentHarness {
104+
geEditButton(filter?: ChipEditHarnessFilters): Promise<MatChipEditHarness>;
92105
getAvatar(filter?: ChipAvatarHarnessFilters): Promise<MatChipAvatarHarness | null>;
93106
getRemoveButton(filter?: ChipRemoveHarnessFilters): Promise<MatChipRemoveHarness>;
94107
getText(): Promise<string>;

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

+10
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ <h4>Multi selection</h4>
160160

161161
<mat-checkbox [(ngModel)]="disableInputs">Disabled</mat-checkbox>
162162
<mat-checkbox [(ngModel)]="editable">Editable</mat-checkbox>
163+
<mat-checkbox [(ngModel)]="peopleWithAvatar">Show Avatar</mat-checkbox>
164+
<mat-checkbox [(ngModel)]="showEditIcon">Show Edit Icon</mat-checkbox>
163165
<mat-checkbox [(ngModel)]="disabledInteractive">Disabled Interactive</mat-checkbox>
164166

165167
<h4>Input is last child of chip grid</h4>
@@ -172,6 +174,14 @@ <h4>Input is last child of chip grid</h4>
172174
[editable]="editable"
173175
(removed)="remove(person)"
174176
(edited)="edit(person, $event)">
177+
@if (showEditIcon) {
178+
<button matChipEdit aria-label="Edit contributor">
179+
<mat-icon>edit</mat-icon>
180+
</button>
181+
}
182+
@if (peopleWithAvatar && person.avatar) {
183+
<mat-chip-avatar>{{person.avatar}}</mat-chip-avatar>
184+
}
175185
{{person.name}}
176186
<button matChipRemove aria-label="Remove contributor">
177187
<mat-icon>close</mat-icon>

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

+9-6
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {MatIconModule} from '@angular/material/icon';
2020

2121
export interface Person {
2222
name: string;
23+
avatar?: string;
2324
}
2425

2526
export interface DemoColor {
@@ -52,6 +53,8 @@ export class ChipsDemo {
5253
listboxesWithAvatar = false;
5354
disableInputs = false;
5455
editable = false;
56+
peopleWithAvatar = false;
57+
showEditIcon = false;
5558
disabledInteractive = false;
5659
message = '';
5760

@@ -75,12 +78,12 @@ export class ChipsDemo {
7578
selectedPeople = null;
7679

7780
people: Person[] = [
78-
{name: 'Kara'},
79-
{name: 'Jeremy'},
80-
{name: 'Topher'},
81-
{name: 'Elad'},
82-
{name: 'Kristiyan'},
83-
{name: 'Paul'},
81+
{name: 'Kara', avatar: 'K'},
82+
{name: 'Jeremy', avatar: 'J'},
83+
{name: 'Topher', avatar: 'T'},
84+
{name: 'Elad', avatar: 'E'},
85+
{name: 'Kristiyan', avatar: 'K'},
86+
{name: 'Paul', avatar: 'P'},
8487
];
8588

8689
availableColors: DemoColor[] = [

src/material/chips/chip-action.ts

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export class MatChipAction {
4343
_handlePrimaryActionInteraction(): void;
4444
remove(): void;
4545
disabled: boolean;
46+
_edit(): void;
4647
_isEditing?: boolean;
4748
}>(MAT_CHIP);
4849

src/material/chips/chip-icons.ts

+50-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import {ENTER, SPACE} from '@angular/cdk/keycodes';
1010
import {Directive} from '@angular/core';
1111
import {MatChipAction} from './chip-action';
12-
import {MAT_CHIP_AVATAR, MAT_CHIP_REMOVE, MAT_CHIP_TRAILING_ICON} from './tokens';
12+
import {MAT_CHIP_AVATAR, MAT_CHIP_EDIT, MAT_CHIP_REMOVE, MAT_CHIP_TRAILING_ICON} from './tokens';
1313

1414
/** Avatar image within a chip. */
1515
@Directive({
@@ -42,6 +42,55 @@ export class MatChipTrailingIcon extends MatChipAction {
4242
override _isPrimary = false;
4343
}
4444

45+
/**
46+
* Directive to edit the parent chip when the leading action icon is clicked or
47+
* when the ENTER key is pressed on it.
48+
*
49+
* Recommended for use with the Material Design "edit" icon
50+
* available at https://material.io/icons/#ic_edit.
51+
*
52+
* Example:
53+
*
54+
* ```
55+
* <mat-chip>
56+
* <button matChipEdit aria-label="Edit">
57+
* <mat-icon>edit</mat-icon>
58+
* </button>
59+
* </mat-chip>
60+
* ```
61+
*/
62+
63+
@Directive({
64+
selector: '[matChipEdit]',
65+
host: {
66+
'class':
67+
'mat-mdc-chip-edit mat-mdc-chip-avatar mat-focus-indicator ' +
68+
'mdc-evolution-chip__icon mdc-evolution-chip__icon--primary',
69+
'role': 'button',
70+
'[attr.aria-hidden]': 'null',
71+
},
72+
providers: [{provide: MAT_CHIP_EDIT, useExisting: MatChipEdit}],
73+
})
74+
export class MatChipEdit extends MatChipAction {
75+
override _isPrimary = false;
76+
77+
override _handleClick(event: MouseEvent): void {
78+
if (!this.disabled) {
79+
event.stopPropagation();
80+
event.preventDefault();
81+
this._parentChip._edit();
82+
}
83+
}
84+
85+
override _handleKeydown(event: KeyboardEvent) {
86+
if ((event.keyCode === ENTER || event.keyCode === SPACE) && !this.disabled) {
87+
event.stopPropagation();
88+
event.preventDefault();
89+
this._parentChip._edit();
90+
}
91+
}
92+
}
93+
4594
/**
4695
* Directive to remove the parent chip when the trailing icon is clicked or
4796
* when the ENTER key is pressed on it.

src/material/chips/chip-row.html

+8-1
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@
22
<span class="mat-mdc-chip-focus-overlay"></span>
33
}
44

5+
@if (!_isEditing && editIcon) {
6+
<span
7+
class="mdc-evolution-chip__cell mdc-evolution-chip__cell--primary"
8+
role="gridcell">
9+
<ng-content select="[matChipEdit]"></ng-content>
10+
</span>
11+
}
512
<span class="mdc-evolution-chip__cell mdc-evolution-chip__cell--primary" role="gridcell"
613
matChipAction
714
[disabled]="disabled"
815
[attr.aria-label]="ariaLabel"
916
[attr.aria-describedby]="_ariaDescriptionId">
10-
@if (leadingIcon) {
17+
@if (!_isEditing && leadingIcon) {
1118
<span class="mdc-evolution-chip__graphic mat-mdc-chip-graphic">
1219
<ng-content select="mat-chip-avatar, [matChipAvatar]"></ng-content>
1320
</span>

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

+19
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,21 @@ describe('Row Chips', () => {
351351
}));
352352
});
353353

354+
describe('with edit icon', () => {
355+
beforeEach(async () => {
356+
testComponent.showEditIcon = true;
357+
fixture.changeDetectorRef.markForCheck();
358+
fixture.detectChanges();
359+
});
360+
361+
it('should begin editing on edit click', () => {
362+
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeFalsy();
363+
dispatchFakeEvent(chipNativeElement.querySelector('.mat-mdc-chip-edit')!, 'click');
364+
fixture.detectChanges();
365+
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeTruthy();
366+
});
367+
});
368+
354369
describe('a11y', () => {
355370
it('should apply `ariaLabel` and `ariaDesciption` to the primary gridcell', () => {
356371
fixture.componentInstance.ariaLabel = 'chip name';
@@ -403,6 +418,9 @@ describe('Row Chips', () => {
403418
(destroyed)="chipDestroy($event)"
404419
(removed)="chipRemove($event)" (edited)="chipEdit($event)"
405420
[aria-label]="ariaLabel" [aria-description]="ariaDescription">
421+
@if (showEditIcon) {
422+
<button matChipEdit>edit</button>
423+
}
406424
{{name}}
407425
<button matChipRemove>x</button>
408426
@if (useCustomEditInput) {
@@ -424,6 +442,7 @@ class SingleChip {
424442
removable: boolean = true;
425443
shouldShow: boolean = true;
426444
editable: boolean = false;
445+
showEditIcon: boolean = false;
427446
useCustomEditInput: boolean = true;
428447
ariaLabel: string | null = null;
429448
ariaDescription: string | null = null;

src/material/chips/chip-row.ts

+21-7
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,16 @@ export interface MatChipEditedEvent extends MatChipEvent {
4141
styleUrl: 'chip.css',
4242
host: {
4343
'class': 'mat-mdc-chip mat-mdc-chip-row mdc-evolution-chip',
44-
'[class.mat-mdc-chip-with-avatar]': 'leadingIcon',
44+
'[class.mat-mdc-chip-with-avatar]': '_hasLeadingIcon()',
4545
'[class.mat-mdc-chip-disabled]': 'disabled',
4646
'[class.mat-mdc-chip-editing]': '_isEditing',
4747
'[class.mat-mdc-chip-editable]': 'editable',
4848
'[class.mdc-evolution-chip--disabled]': 'disabled',
49+
'[class.mdc-evolution-chip--with-leading-action]': '!!editIcon',
4950
'[class.mdc-evolution-chip--with-trailing-action]': '_hasTrailingIcon()',
50-
'[class.mdc-evolution-chip--with-primary-graphic]': 'leadingIcon',
51-
'[class.mdc-evolution-chip--with-primary-icon]': 'leadingIcon',
52-
'[class.mdc-evolution-chip--with-avatar]': 'leadingIcon',
51+
'[class.mdc-evolution-chip--with-primary-graphic]': '_hasLeadingIcon()',
52+
'[class.mdc-evolution-chip--with-primary-icon]': '_hasLeadingIcon()',
53+
'[class.mdc-evolution-chip--with-avatar]': '_hasLeadingIcon()',
5354
'[class.mat-mdc-chip-highlighted]': 'highlighted',
5455
'[class.mat-mdc-chip-with-trailing-icon]': '_hasTrailingIcon()',
5556
'[id]': 'id',
@@ -107,6 +108,11 @@ export class MatChipRow extends MatChip implements AfterViewInit {
107108
});
108109
}
109110

111+
/** Returns whether the chip has a leading icon. */
112+
_hasLeadingIcon() {
113+
return !this._isEditing && !!(this.editIcon || this.leadingIcon);
114+
}
115+
110116
override _hasTrailingIcon() {
111117
// The trailing icon is hidden while editing.
112118
return !this._isEditing && super._hasTrailingIcon();
@@ -141,10 +147,18 @@ export class MatChipRow extends MatChip implements AfterViewInit {
141147
}
142148
}
143149

144-
private _startEditing(event: Event) {
150+
override _edit(): void {
151+
// markForCheck necessary for edit input to be rendered
152+
this._changeDetectorRef.markForCheck();
153+
this._startEditing();
154+
}
155+
156+
private _startEditing(event?: Event) {
145157
if (
146158
!this.primaryAction ||
147-
(this.removeIcon && this._getSourceAction(event.target as Node) === this.removeIcon)
159+
(this.removeIcon &&
160+
!!event &&
161+
this._getSourceAction(event.target as Node) === this.removeIcon)
148162
) {
149163
return;
150164
}
@@ -158,7 +172,7 @@ export class MatChipRow extends MatChip implements AfterViewInit {
158172
afterNextRender(
159173
() => {
160174
this._getEditInput().initialize(value);
161-
this._editStartPending = false;
175+
setTimeout(() => this._ngZone.run(() => (this._editStartPending = false)));
162176
},
163177
{injector: this._injector},
164178
);

0 commit comments

Comments
 (0)