Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 2438454

Browse files
authoredMay 30, 2025··
feat(material/chips): add (optional) edit icon to input chips (#31041)
1 parent 203c173 commit 2438454

File tree

19 files changed

+275
-24
lines changed

19 files changed

+275
-24
lines changed
 

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

Lines changed: 29 additions & 3 deletions
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,22 @@ 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+
_isLeading: boolean;
149+
// (undocumented)
150+
_isPrimary: boolean;
151+
// (undocumented)
152+
static ɵdir: i0.ɵɵDirectiveDeclaration<MatChipEdit, "[matChipEdit]", never, {}, {}, never, never, true, never>;
153+
// (undocumented)
154+
static ɵfac: i0.ɵɵFactoryDeclaration<MatChipEdit, never>;
155+
}
156+
135157
// @public
136158
export interface MatChipEditedEvent extends MatChipEvent {
137159
value: string;
@@ -420,6 +442,8 @@ export class MatChipRow extends MatChip implements AfterViewInit {
420442
contentEditInput?: MatChipEditInput;
421443
defaultEditInput?: MatChipEditInput;
422444
// (undocumented)
445+
_edit(): void;
446+
// (undocumented)
423447
editable: boolean;
424448
readonly edited: EventEmitter<MatChipEditedEvent>;
425449
// (undocumented)
@@ -430,6 +454,8 @@ export class MatChipRow extends MatChip implements AfterViewInit {
430454
// (undocumented)
431455
_handleKeydown(event: KeyboardEvent): void;
432456
// (undocumented)
457+
protected _hasLeadingActionIcon(): boolean;
458+
// (undocumented)
433459
_hasTrailingIcon(): boolean;
434460
// (undocumented)
435461
_isEditing: boolean;
@@ -438,7 +464,7 @@ export class MatChipRow extends MatChip implements AfterViewInit {
438464
// (undocumented)
439465
ngAfterViewInit(): void;
440466
// (undocumented)
441-
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>;
467+
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>;
442468
// (undocumented)
443469
static ɵfac: i0.ɵɵFactoryDeclaration<MatChipRow, never>;
444470
}
@@ -515,7 +541,7 @@ export class MatChipsModule {
515541
// (undocumented)
516542
static ɵinj: i0.ɵɵInjectorDeclaration<MatChipsModule>;
517543
// (undocumented)
518-
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]>;
544+
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]>;
519545
}
520546

521547
// @public

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

Lines changed: 13 additions & 0 deletions
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/components-examples/material/chips/chips-input/chips-input-example.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
(edited)="edit(fruit, $event)"
99
[aria-description]="'press enter to edit ' + fruit.name"
1010
>
11+
<button matChipEdit [attr.aria-label]="'edit ' + fruit.name">
12+
<mat-icon>edit</mat-icon>
13+
</button>
1114
{{fruit.name}}
1215
<button matChipRemove [attr.aria-label]="'remove ' + fruit.name">
1316
<mat-icon>cancel</mat-icon>

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

Lines changed: 10 additions & 0 deletions
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

Lines changed: 9 additions & 6 deletions
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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import {_StructuralStylesLoader} from '../core';
2929
'class': 'mdc-evolution-chip__action mat-mdc-chip-action',
3030
'[class.mdc-evolution-chip__action--primary]': '_isPrimary',
3131
'[class.mdc-evolution-chip__action--presentational]': '!isInteractive',
32-
'[class.mdc-evolution-chip__action--trailing]': '!_isPrimary',
32+
'[class.mdc-evolution-chip__action--secondary]': '!_isPrimary',
33+
'[class.mdc-evolution-chip__action--trailing]': '!_isPrimary && !_isLeading',
3334
'[attr.tabindex]': '_getTabindex()',
3435
'[attr.disabled]': '_getDisabledAttribute()',
3536
'[attr.aria-disabled]': 'disabled',
@@ -43,6 +44,7 @@ export class MatChipAction {
4344
_handlePrimaryActionInteraction(): void;
4445
remove(): void;
4546
disabled: boolean;
47+
_edit(): void;
4648
_isEditing?: boolean;
4749
}>(MAT_CHIP);
4850

@@ -52,6 +54,9 @@ export class MatChipAction {
5254
/** Whether this is the primary action in the chip. */
5355
_isPrimary = true;
5456

57+
/** Whether this is the leading action in the chip. */
58+
_isLeading = false; // TODO(adolgachev): consolidate usage to secondary css class
59+
5560
/** Whether the action is disabled. */
5661
@Input({transform: booleanAttribute})
5762
get disabled(): boolean {

‎src/material/chips/chip-grid.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ describe('MatChipGrid', () => {
270270
fixture = createComponent(ChipGridWithRemove);
271271
flush();
272272
trailingActions = chipGridNativeElement.querySelectorAll(
273-
'.mdc-evolution-chip__action--trailing',
273+
'.mdc-evolution-chip__action--secondary',
274274
);
275275
}));
276276

@@ -595,7 +595,7 @@ describe('MatChipGrid', () => {
595595
const fixture = createComponent(ChipGridWithRemove, undefined, [NoopAnimationsModule]);
596596
flush();
597597
const trailingActions = chipGridNativeElement.querySelectorAll<HTMLElement>(
598-
'.mdc-evolution-chip__action--trailing',
598+
'.mdc-evolution-chip__action--secondary',
599599
);
600600
const chip = chips.get(2)!;
601601
chip.focus();

‎src/material/chips/chip-icons.ts

Lines changed: 51 additions & 1 deletion
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,56 @@ 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+
override _isLeading = true;
77+
78+
override _handleClick(event: MouseEvent): void {
79+
if (!this.disabled) {
80+
event.stopPropagation();
81+
event.preventDefault();
82+
this._parentChip._edit();
83+
}
84+
}
85+
86+
override _handleKeydown(event: KeyboardEvent) {
87+
if ((event.keyCode === ENTER || event.keyCode === SPACE) && !this.disabled) {
88+
event.stopPropagation();
89+
event.preventDefault();
90+
this._parentChip._edit();
91+
}
92+
}
93+
}
94+
4595
/**
4696
* Directive to remove the parent chip when the trailing icon is clicked or
4797
* when the ENTER key is pressed on it.

‎src/material/chips/chip-row.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
<span class="mat-mdc-chip-focus-overlay"></span>
33
}
44

5+
@if (_hasLeadingActionIcon()) {
6+
<span class="mdc-evolution-chip__cell mdc-evolution-chip__cell--leading" role="gridcell">
7+
<ng-content select="[matChipEdit]"></ng-content>
8+
</span>
9+
}
510
<span class="mdc-evolution-chip__cell mdc-evolution-chip__cell--primary" role="gridcell"
611
matChipAction
712
[disabled]="disabled"

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,21 @@ describe('Row Chips', () => {
436436
}));
437437
});
438438

439+
describe('with edit icon', () => {
440+
beforeEach(async () => {
441+
testComponent.showEditIcon = true;
442+
fixture.changeDetectorRef.markForCheck();
443+
fixture.detectChanges();
444+
});
445+
446+
it('should begin editing on edit click', () => {
447+
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeFalsy();
448+
dispatchFakeEvent(chipNativeElement.querySelector('.mat-mdc-chip-edit')!, 'click');
449+
fixture.detectChanges();
450+
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeTruthy();
451+
});
452+
});
453+
439454
describe('a11y', () => {
440455
it('should apply `ariaLabel` and `ariaDesciption` to the primary gridcell', () => {
441456
fixture.componentInstance.ariaLabel = 'chip name';
@@ -488,6 +503,9 @@ describe('Row Chips', () => {
488503
(destroyed)="chipDestroy($event)"
489504
(removed)="chipRemove($event)" (edited)="chipEdit($event)"
490505
[aria-label]="ariaLabel" [aria-description]="ariaDescription">
506+
@if (showEditIcon) {
507+
<button matChipEdit>edit</button>
508+
}
491509
{{name}}
492510
<button matChipRemove>x</button>
493511
@if (useCustomEditInput) {
@@ -509,6 +527,7 @@ class SingleChip {
509527
removable: boolean = true;
510528
shouldShow: boolean = true;
511529
editable: boolean = false;
530+
showEditIcon: boolean = false;
512531
useCustomEditInput: boolean = true;
513532
ariaLabel: string | null = null;
514533
ariaDescription: string | null = null;

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export interface MatChipEditedEvent extends MatChipEvent {
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]': '_hasLeadingActionIcon()',
4950
'[class.mdc-evolution-chip--with-trailing-action]': '_hasTrailingIcon()',
5051
'[class.mdc-evolution-chip--with-primary-graphic]': 'leadingIcon',
5152
'[class.mdc-evolution-chip--with-primary-icon]': 'leadingIcon',
@@ -130,6 +131,11 @@ export class MatChipRow extends MatChip implements AfterViewInit {
130131
});
131132
}
132133

134+
protected _hasLeadingActionIcon() {
135+
// The leading action (edit) icon is hidden while editing.
136+
return !this._isEditing && !!this.editIcon;
137+
}
138+
133139
override _hasTrailingIcon() {
134140
// The trailing icon is hidden while editing.
135141
return !this._isEditing && super._hasTrailingIcon();
@@ -174,10 +180,18 @@ export class MatChipRow extends MatChip implements AfterViewInit {
174180
}
175181
}
176182

177-
private _startEditing(event: Event) {
183+
override _edit(): void {
184+
// markForCheck necessary for edit input to be rendered
185+
this._changeDetectorRef.markForCheck();
186+
this._startEditing();
187+
}
188+
189+
private _startEditing(event?: Event) {
178190
if (
179191
!this.primaryAction ||
180-
(this.removeIcon && this._getSourceAction(event.target as Node) === this.removeIcon)
192+
(this.removeIcon &&
193+
!!event &&
194+
this._getSourceAction(event.target as Node) === this.removeIcon)
181195
) {
182196
return;
183197
}
@@ -191,7 +205,9 @@ export class MatChipRow extends MatChip implements AfterViewInit {
191205
afterNextRender(
192206
() => {
193207
this._getEditInput().initialize(value);
194-
this._editStartPending = false;
208+
209+
// Necessary when using edit icon to prevent edit from aborting
210+
setTimeout(() => this._ngZone.run(() => (this._editStartPending = false)));
195211
},
196212
{injector: this._injector},
197213
);

‎src/material/chips/chip.scss

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,19 @@ $fallbacks: m3-chip.get-tokens();
130130

131131
// Moved out into variables, because the selectors are too long.
132132
$with-graphic: '.mdc-evolution-chip--with-primary-graphic';
133+
$with-leading: '.mdc-evolution-chip--with-leading-action';
133134
$with-trailing: '.mdc-evolution-chip--with-trailing-action';
134135

136+
.mat-mdc-standard-chip#{$with-leading} & {
137+
padding-left: 0;
138+
padding-right: $_action-padding;
139+
}
140+
141+
[dir='rtl'] .mat-mdc-standard-chip#{$with-leading} & {
142+
padding-left: $_action-padding;
143+
padding-right: 0;
144+
}
145+
135146
.mat-mdc-standard-chip#{$with-trailing} & {
136147
padding-left: $_action-padding;
137148
padding-right: 0;
@@ -142,6 +153,11 @@ $fallbacks: m3-chip.get-tokens();
142153
padding-right: $_action-padding;
143154
}
144155

156+
.mat-mdc-standard-chip#{$with-leading}#{$with-trailing} & {
157+
padding-left: 0;
158+
padding-right: 0;
159+
}
160+
145161
.mat-mdc-standard-chip#{$with-graphic}#{$with-trailing} & {
146162
padding-left: 0;
147163
padding-right: 0;
@@ -173,7 +189,7 @@ $fallbacks: m3-chip.get-tokens();
173189
}
174190
}
175191

176-
.mdc-evolution-chip__action--trailing {
192+
.mdc-evolution-chip__action--secondary {
177193
position: relative;
178194
overflow: visible;
179195

@@ -199,7 +215,6 @@ $fallbacks: m3-chip.get-tokens();
199215
padding-right: $_trailing-action-padding;
200216
}
201217

202-
203218
.mdc-evolution-chip--with-avatar#{$with-graphic}#{$with-trailing} & {
204219
padding-left: $_avatar-trailing-padding;
205220
padding-right: $_avatar-trailing-padding;
@@ -262,6 +277,7 @@ $fallbacks: m3-chip.get-tokens();
262277
// Moved out into variables, because the selectors are too long.
263278
$with-icon: '.mdc-evolution-chip--with-primary-icon';
264279
$with-graphic: '.mdc-evolution-chip--with-primary-graphic';
280+
$with-leading: '.mdc-evolution-chip--with-leading-action';
265281
$with-trailing: '.mdc-evolution-chip--with-trailing-action';
266282

267283
.mdc-evolution-chip--selectable:not(.mdc-evolution-chip--selected):not(#{$with-icon}) & {
@@ -297,6 +313,10 @@ $fallbacks: m3-chip.get-tokens();
297313
padding-left: $_avatar-trailing-padding;
298314
padding-right: $_avatar-leading-padding;
299315
}
316+
317+
.mdc-evolution-chip--with-avatar#{$with-graphic}#{$with-leading} & {
318+
padding-left: 0;
319+
}
300320
}
301321

302322
.mdc-evolution-chip__checkmark {
@@ -499,7 +519,7 @@ $fallbacks: m3-chip.get-tokens();
499519
}
500520
}
501521

502-
.mat-mdc-chip-remove {
522+
.mat-mdc-chip-edit, .mat-mdc-chip-remove {
503523
opacity: token-utils.slot(chip-trailing-action-opacity, $fallbacks);
504524

505525
&:focus {
@@ -650,7 +670,7 @@ $fallbacks: m3-chip.get-tokens();
650670
}
651671
}
652672

653-
.mat-mdc-chip-remove {
673+
.mat-mdc-chip-edit, .mat-mdc-chip-remove {
654674
&::before {
655675
$default-border-width: focus-indicators-private.$default-border-width;
656676
$offset: var(--mat-focus-indicator-border-width, #{$default-border-width});
@@ -714,6 +734,6 @@ $fallbacks: m3-chip.get-tokens();
714734
// Prevents icon from being cut off when text spacing is increased.
715735
// .mat-mdc-chip-remove selector necessary for remove button with icon.
716736
// Fixes b/250063405.
717-
.mdc-evolution-chip__icon, .mat-mdc-chip-remove .mat-icon {
737+
.mdc-evolution-chip__icon, .mat-mdc-chip-edit .mat-icon, .mat-mdc-chip-remove .mat-icon {
718738
min-height: fit-content;
719739
}

‎src/material/chips/chip.ts

Lines changed: 25 additions & 2 deletions
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

Lines changed: 2 additions & 1 deletion
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,
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {
10+
ComponentHarness,
11+
ComponentHarnessConstructor,
12+
HarnessPredicate,
13+
} from '@angular/cdk/testing';
14+
import {ChipEditHarnessFilters} from './chip-harness-filters';
15+
16+
/** Harness for interacting with a standard Material chip edit button in tests. */
17+
export class MatChipEditHarness extends ComponentHarness {
18+
static hostSelector = '.mat-mdc-chip-edit';
19+
20+
/**
21+
* Gets a `HarnessPredicate` that can be used to search for a chip edit with specific
22+
* attributes.
23+
* @param options Options for filtering which input instances are considered a match.
24+
* @return a `HarnessPredicate` configured with the given options.
25+
*/
26+
static with<T extends MatChipEditHarness>(
27+
this: ComponentHarnessConstructor<T>,
28+
options: ChipEditHarnessFilters = {},
29+
): HarnessPredicate<T> {
30+
return new HarnessPredicate(this, options);
31+
}
32+
33+
/** Clicks the edit button. */
34+
async click(): Promise<void> {
35+
return (await this.host()).click();
36+
}
37+
}

‎src/material/chips/testing/chip-harness-filters.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export interface ChipRowHarnessFilters extends ChipHarnessFilters {}
4343

4444
export interface ChipSetHarnessFilters extends BaseHarnessFilters {}
4545

46+
export interface ChipEditHarnessFilters extends BaseHarnessFilters {}
47+
4648
export interface ChipRemoveHarnessFilters extends BaseHarnessFilters {}
4749

4850
export interface ChipAvatarHarnessFilters extends BaseHarnessFilters {}

‎src/material/chips/testing/chip-harness.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ import {
1515
import {MatChipAvatarHarness} from './chip-avatar-harness';
1616
import {
1717
ChipAvatarHarnessFilters,
18+
ChipEditHarnessFilters,
1819
ChipHarnessFilters,
1920
ChipRemoveHarnessFilters,
2021
} from './chip-harness-filters';
22+
import {MatChipEditHarness} from './chip-edit-harness';
2123
import {MatChipRemoveHarness} from './chip-remove-harness';
2224

2325
/** Harness for interacting with a mat-chip in tests. */
@@ -62,6 +64,14 @@ export class MatChipHarness extends ContentContainerComponentHarness {
6264
await hostEl.sendKeys(TestKey.DELETE);
6365
}
6466

67+
/**
68+
* Gets the edit button inside of a chip.
69+
* @param filter Optionally filters which chips are included.
70+
*/
71+
async geEditButton(filter: ChipEditHarnessFilters = {}): Promise<MatChipEditHarness> {
72+
return this.locatorFor(MatChipEditHarness.with(filter))();
73+
}
74+
6575
/**
6676
* Gets the remove button inside of a chip.
6777
* @param filter Optionally filters which chips are included.

‎src/material/chips/testing/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
export * from './chip-avatar-harness';
10+
export * from './chip-edit-harness';
1011
export * from './chip-harness';
1112
export * from './chip-harness-filters';
1213
export * from './chip-input-harness';

‎src/material/chips/tokens.ts

Lines changed: 7 additions & 0 deletions
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)
Please sign in to comment.