Skip to content

Commit c3dfdb3

Browse files
committed
feat(material/chips): add (optional) edit icon to input chips
1 parent d6b6bce commit c3dfdb3

File tree

9 files changed

+122
-17
lines changed

9 files changed

+122
-17
lines changed

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

+3
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@ <h4>Input is last child of chip grid</h4>
172172
[editable]="editable"
173173
(removed)="remove(person)"
174174
(edited)="edit(person, $event)">
175+
<button matChipEdit aria-label="Edit contributor">
176+
<mat-icon>edit</mat-icon>
177+
</button>
175178
{{person.name}}
176179
<button matChipRemove aria-label="Remove contributor">
177180
<mat-icon>close</mat-icon>

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

+48-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,53 @@ export class MatChipTrailingIcon extends MatChipAction {
4242
override _isPrimary = false;
4343
}
4444

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

+25-9
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import {takeUntil} from 'rxjs/operators';
2323
import {MatChip, MatChipEvent} from './chip';
2424
import {MatChipAction} from './chip-action';
2525
import {MatChipEditInput} from './chip-edit-input';
26-
import {MAT_CHIP} from './tokens';
26+
import {MatChipEdit} from './chip-icons';
27+
import {MAT_CHIP, MAT_CHIP_EDIT} from './tokens';
2728

2829
/** Represents an event fired on an individual `mat-chip` when it is edited. */
2930
export interface MatChipEditedEvent extends MatChipEvent {
@@ -41,15 +42,15 @@ export interface MatChipEditedEvent extends MatChipEvent {
4142
styleUrl: 'chip.css',
4243
host: {
4344
'class': 'mat-mdc-chip mat-mdc-chip-row mdc-evolution-chip',
44-
'[class.mat-mdc-chip-with-avatar]': 'leadingIcon',
45+
'[class.mat-mdc-chip-with-avatar]': '_hasLeadingIcon()',
4546
'[class.mat-mdc-chip-disabled]': 'disabled',
4647
'[class.mat-mdc-chip-editing]': '_isEditing',
4748
'[class.mat-mdc-chip-editable]': 'editable',
4849
'[class.mdc-evolution-chip--disabled]': 'disabled',
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();
@@ -135,16 +141,26 @@ export class MatChipRow extends MatChip implements AfterViewInit {
135141
}
136142
}
137143

138-
_handleDoubleclick(event: MouseEvent) {
144+
_handleDoubleclick(event: Event) {
139145
if (!this.disabled && this.editable) {
140146
this._startEditing(event);
141147
}
142148
}
143149

144-
private _startEditing(event: Event) {
150+
override _edit(): void {
151+
if (!this.disabled && this.editable) {
152+
// markForCheck necessary for edit input to be rendered
153+
this._changeDetectorRef.markForCheck();
154+
this._startEditing();
155+
}
156+
}
157+
158+
private _startEditing(event?: Event) {
145159
if (
146160
!this.primaryAction ||
147-
(this.removeIcon && this._getSourceAction(event.target as Node) === this.removeIcon)
161+
(this.removeIcon &&
162+
!!event &&
163+
this._getSourceAction(event.target as Node) === this.removeIcon)
148164
) {
149165
return;
150166
}
@@ -158,7 +174,7 @@ export class MatChipRow extends MatChip implements AfterViewInit {
158174
afterNextRender(
159175
() => {
160176
this._getEditInput().initialize(value);
161-
this._editStartPending = false;
177+
setTimeout(() => this._ngZone.run(() => (this._editStartPending = false)));
162178
},
163179
{injector: this._injector},
164180
);

src/material/chips/chip.scss

+3-3
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,7 @@ $token-slots: m2-chip.get-token-slots();
528528
}
529529
}
530530

531-
.mat-mdc-chip-remove {
531+
.mat-mdc-chip-edit, .mat-mdc-chip-remove {
532532
opacity: token-utils.slot(trailing-action-opacity);
533533

534534
&:focus {
@@ -680,7 +680,7 @@ $token-slots: m2-chip.get-token-slots();
680680
}
681681
}
682682

683-
.mat-mdc-chip-remove {
683+
.mat-mdc-chip-edit, .mat-mdc-chip-remove {
684684
&::before {
685685
$default-border-width: focus-indicators-private.$default-border-width;
686686
$offset: var(--mat-focus-indicator-border-width, #{$default-border-width});
@@ -744,6 +744,6 @@ $token-slots: m2-chip.get-token-slots();
744744
// Prevents icon from being cut off when text spacing is increased.
745745
// .mat-mdc-chip-remove selector necessary for remove button with icon.
746746
// Fixes b/250063405.
747-
.mdc-evolution-chip__icon, .mat-mdc-chip-remove .mat-icon {
747+
.mdc-evolution-chip__icon, .mat-mdc-chip-edit .mat-icon, .mat-mdc-chip-remove .mat-icon {
748748
min-height: fit-content;
749749
}

src/material/chips/chip.ts

+25-2
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,14 @@ import {
4444
} from '../core';
4545
import {Subject, Subscription, merge} from 'rxjs';
4646
import {MatChipAction} from './chip-action';
47-
import {MatChipAvatar, MatChipRemove, MatChipTrailingIcon} from './chip-icons';
48-
import {MAT_CHIP, MAT_CHIP_AVATAR, MAT_CHIP_REMOVE, MAT_CHIP_TRAILING_ICON} from './tokens';
47+
import {MatChipAvatar, MatChipEdit, MatChipRemove, MatChipTrailingIcon} from './chip-icons';
48+
import {
49+
MAT_CHIP,
50+
MAT_CHIP_AVATAR,
51+
MAT_CHIP_EDIT,
52+
MAT_CHIP_REMOVE,
53+
MAT_CHIP_TRAILING_ICON,
54+
} from './tokens';
4955

5056
/** Represents an event fired on an individual `mat-chip`. */
5157
export interface MatChipEvent {
@@ -133,6 +139,10 @@ export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck
133139
@ContentChildren(MAT_CHIP_TRAILING_ICON, {descendants: true})
134140
protected _allTrailingIcons: QueryList<MatChipTrailingIcon>;
135141

142+
/** All edit icons present in the chip. */
143+
@ContentChildren(MAT_CHIP_EDIT, {descendants: true})
144+
protected _allEditIcons: QueryList<MatChipEdit>;
145+
136146
/** All remove icons present in the chip. */
137147
@ContentChildren(MAT_CHIP_REMOVE, {descendants: true})
138148
protected _allRemoveIcons: QueryList<MatChipRemove>;
@@ -225,6 +235,9 @@ export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck
225235
/** The chip's leading icon. */
226236
@ContentChild(MAT_CHIP_AVATAR) leadingIcon: MatChipAvatar;
227237

238+
/** The chip's leading edit icon. */
239+
@ContentChild(MAT_CHIP_EDIT) editIcon: MatChipEdit;
240+
228241
/** The chip's trailing icon. */
229242
@ContentChild(MAT_CHIP_TRAILING_ICON) trailingIcon: MatChipTrailingIcon;
230243

@@ -279,6 +292,7 @@ export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck
279292
this._actionChanges = merge(
280293
this._allLeadingIcons.changes,
281294
this._allTrailingIcons.changes,
295+
this._allEditIcons.changes,
282296
this._allRemoveIcons.changes,
283297
).subscribe(() => this._changeDetectorRef.markForCheck());
284298
}
@@ -358,6 +372,10 @@ export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck
358372
_getActions(): MatChipAction[] {
359373
const result: MatChipAction[] = [];
360374

375+
if (this.editIcon) {
376+
result.push(this.editIcon);
377+
}
378+
361379
if (this.primaryAction) {
362380
result.push(this.primaryAction);
363381
}
@@ -378,6 +396,11 @@ export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck
378396
// Empty here, but is overwritten in child classes.
379397
}
380398

399+
/** Handles interactions with the edit action of the chip. */
400+
_edit(event: Event) {
401+
// Empty here, but is overwritten in child classes.
402+
}
403+
381404
/** Starts the focus monitoring process on the chip. */
382405
private _monitorFocus() {
383406
this._focusMonitor.monitor(this._elementRef, true).subscribe(origin => {

src/material/chips/module.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {MatChip} from './chip';
1313
import {MAT_CHIPS_DEFAULT_OPTIONS, MatChipsDefaultOptions} from './tokens';
1414
import {MatChipEditInput} from './chip-edit-input';
1515
import {MatChipGrid} from './chip-grid';
16-
import {MatChipAvatar, MatChipRemove, MatChipTrailingIcon} from './chip-icons';
16+
import {MatChipAvatar, MatChipEdit, MatChipRemove, MatChipTrailingIcon} from './chip-icons';
1717
import {MatChipInput} from './chip-input';
1818
import {MatChipListbox} from './chip-listbox';
1919
import {MatChipRow} from './chip-row';
@@ -24,6 +24,7 @@ import {MatChipAction} from './chip-action';
2424
const CHIP_DECLARATIONS = [
2525
MatChip,
2626
MatChipAvatar,
27+
MatChipEdit,
2728
MatChipEditInput,
2829
MatChipGrid,
2930
MatChipInput,

src/material/chips/tokens.ts

+7
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ export const MAT_CHIP_AVATAR = new InjectionToken('MatChipAvatar');
4646
*/
4747
export const MAT_CHIP_TRAILING_ICON = new InjectionToken('MatChipTrailingIcon');
4848

49+
/**
50+
* Injection token that can be used to reference instances of `MatChipEdit`. It serves as
51+
* alternative token to the actual `MatChipEdit` class which could cause unnecessary
52+
* retention of the class and its directive metadata.
53+
*/
54+
export const MAT_CHIP_EDIT = new InjectionToken('MatChipEdit');
55+
4956
/**
5057
* Injection token that can be used to reference instances of `MatChipRemove`. It serves as
5158
* alternative token to the actual `MatChipRemove` class which could cause unnecessary

0 commit comments

Comments
 (0)