From fe5ea55dc70f15e5ab380183caf429883d19a38c Mon Sep 17 00:00:00 2001
From: Andrey Dolgachev <adolgachev@users.noreply.github.com>
Date: Fri, 2 May 2025 12:57:41 -0700
Subject: [PATCH] feat(material/chips): add (optional) edit icon to input chips

---
 goldens/material/chips/index.api.md           | 32 ++++++++++--
 goldens/material/chips/testing/index.api.md   | 13 +++++
 .../chips-input/chips-input-example.html      |  3 ++
 src/dev-app/chips/chips-demo.html             | 10 ++++
 src/dev-app/chips/chips-demo.ts               | 15 +++---
 src/material/chips/chip-action.ts             |  7 ++-
 src/material/chips/chip-grid.spec.ts          |  4 +-
 src/material/chips/chip-icons.ts              | 52 ++++++++++++++++++-
 src/material/chips/chip-row.html              |  5 ++
 src/material/chips/chip-row.spec.ts           | 19 +++++++
 src/material/chips/chip-row.ts                | 22 ++++++--
 src/material/chips/chip.scss                  | 30 +++++++++--
 src/material/chips/chip.ts                    | 27 +++++++++-
 src/material/chips/module.ts                  |  3 +-
 .../chips/testing/chip-edit-harness.ts        | 37 +++++++++++++
 .../chips/testing/chip-harness-filters.ts     |  2 +
 src/material/chips/testing/chip-harness.ts    | 10 ++++
 src/material/chips/testing/public-api.ts      |  1 +
 src/material/chips/tokens.ts                  |  7 +++
 19 files changed, 275 insertions(+), 24 deletions(-)
 create mode 100644 src/material/chips/testing/chip-edit-harness.ts

diff --git a/goldens/material/chips/index.api.md b/goldens/material/chips/index.api.md
index 8c3c61b45309..fdcc60891262 100644
--- a/goldens/material/chips/index.api.md
+++ b/goldens/material/chips/index.api.md
@@ -35,6 +35,9 @@ export const MAT_CHIP: InjectionToken<unknown>;
 // @public
 export const MAT_CHIP_AVATAR: InjectionToken<unknown>;
 
+// @public
+export const MAT_CHIP_EDIT: InjectionToken<unknown>;
+
 // @public
 export const MAT_CHIP_LISTBOX_CONTROL_VALUE_ACCESSOR: any;
 
@@ -50,6 +53,7 @@ export const MAT_CHIPS_DEFAULT_OPTIONS: InjectionToken<MatChipsDefaultOptions>;
 // @public
 export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck, OnDestroy {
     constructor(...args: unknown[]);
+    protected _allEditIcons: QueryList<MatChipEdit>;
     protected _allLeadingIcons: QueryList<MatChipAvatar>;
     protected _allRemoveIcons: QueryList<MatChipRemove>;
     protected _allTrailingIcons: QueryList<MatChipTrailingIcon>;
@@ -68,6 +72,8 @@ export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck
     disableRipple: boolean;
     // (undocumented)
     protected _document: Document;
+    _edit(event: Event): void;
+    editIcon: MatChipEdit;
     // (undocumented)
     _elementRef: ElementRef<HTMLElement>;
     focus(): void;
@@ -119,7 +125,7 @@ export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck
     // (undocumented)
     protected _value: any;
     // (undocumented)
-    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>;
+    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>;
     // (undocumented)
     static ɵfac: i0.ɵɵFactoryDeclaration<MatChip, never>;
 }
@@ -132,6 +138,22 @@ export class MatChipAvatar {
     static ɵfac: i0.ɵɵFactoryDeclaration<MatChipAvatar, never>;
 }
 
+// @public
+export class MatChipEdit extends MatChipAction {
+    // (undocumented)
+    _handleClick(event: MouseEvent): void;
+    // (undocumented)
+    _handleKeydown(event: KeyboardEvent): void;
+    // (undocumented)
+    _isLeading: boolean;
+    // (undocumented)
+    _isPrimary: boolean;
+    // (undocumented)
+    static ɵdir: i0.ɵɵDirectiveDeclaration<MatChipEdit, "[matChipEdit]", never, {}, {}, never, never, true, never>;
+    // (undocumented)
+    static ɵfac: i0.ɵɵFactoryDeclaration<MatChipEdit, never>;
+}
+
 // @public
 export interface MatChipEditedEvent extends MatChipEvent {
     value: string;
@@ -420,6 +442,8 @@ export class MatChipRow extends MatChip implements AfterViewInit {
     contentEditInput?: MatChipEditInput;
     defaultEditInput?: MatChipEditInput;
     // (undocumented)
+    _edit(): void;
+    // (undocumented)
     editable: boolean;
     readonly edited: EventEmitter<MatChipEditedEvent>;
     // (undocumented)
@@ -430,6 +454,8 @@ export class MatChipRow extends MatChip implements AfterViewInit {
     // (undocumented)
     _handleKeydown(event: KeyboardEvent): void;
     // (undocumented)
+    protected _hasLeadingActionIcon(): boolean;
+    // (undocumented)
     _hasTrailingIcon(): boolean;
     // (undocumented)
     _isEditing: boolean;
@@ -438,7 +464,7 @@ export class MatChipRow extends MatChip implements AfterViewInit {
     // (undocumented)
     ngAfterViewInit(): void;
     // (undocumented)
-    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>;
+    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>;
     // (undocumented)
     static ɵfac: i0.ɵɵFactoryDeclaration<MatChipRow, never>;
 }
@@ -515,7 +541,7 @@ export class MatChipsModule {
     // (undocumented)
     static ɵinj: i0.ɵɵInjectorDeclaration<MatChipsModule>;
     // (undocumented)
-    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]>;
+    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]>;
 }
 
 // @public
diff --git a/goldens/material/chips/testing/index.api.md b/goldens/material/chips/testing/index.api.md
index f23dead77a18..150b9d3fe079 100644
--- a/goldens/material/chips/testing/index.api.md
+++ b/goldens/material/chips/testing/index.api.md
@@ -16,6 +16,10 @@ import { TestKey } from '@angular/cdk/testing';
 export interface ChipAvatarHarnessFilters extends BaseHarnessFilters {
 }
 
+// @public (undocumented)
+export interface ChipEditHarnessFilters extends BaseHarnessFilters {
+}
+
 // @public (undocumented)
 export interface ChipEditInputHarnessFilters extends BaseHarnessFilters {
 }
@@ -67,6 +71,14 @@ export class MatChipAvatarHarness extends ComponentHarness {
     static with<T extends MatChipAvatarHarness>(this: ComponentHarnessConstructor<T>, options?: ChipAvatarHarnessFilters): HarnessPredicate<T>;
 }
 
+// @public
+export class MatChipEditHarness extends ComponentHarness {
+    click(): Promise<void>;
+    // (undocumented)
+    static hostSelector: string;
+    static with<T extends MatChipEditHarness>(this: ComponentHarnessConstructor<T>, options?: ChipEditHarnessFilters): HarnessPredicate<T>;
+}
+
 // @public
 export class MatChipEditInputHarness extends ComponentHarness {
     // (undocumented)
@@ -89,6 +101,7 @@ export class MatChipGridHarness extends ComponentHarness {
 
 // @public
 export class MatChipHarness extends ContentContainerComponentHarness {
+    geEditButton(filter?: ChipEditHarnessFilters): Promise<MatChipEditHarness>;
     getAvatar(filter?: ChipAvatarHarnessFilters): Promise<MatChipAvatarHarness | null>;
     getRemoveButton(filter?: ChipRemoveHarnessFilters): Promise<MatChipRemoveHarness>;
     getText(): Promise<string>;
diff --git a/src/components-examples/material/chips/chips-input/chips-input-example.html b/src/components-examples/material/chips/chips-input/chips-input-example.html
index 3d29e5442015..b1e75ee2426a 100644
--- a/src/components-examples/material/chips/chips-input/chips-input-example.html
+++ b/src/components-examples/material/chips/chips-input/chips-input-example.html
@@ -8,6 +8,9 @@
         (edited)="edit(fruit, $event)"
         [aria-description]="'press enter to edit ' + fruit.name"
       >
+        <button matChipEdit [attr.aria-label]="'edit ' + fruit.name">
+          <mat-icon>edit</mat-icon>
+        </button>      
         {{fruit.name}}
         <button matChipRemove [attr.aria-label]="'remove ' + fruit.name">
           <mat-icon>cancel</mat-icon>
diff --git a/src/dev-app/chips/chips-demo.html b/src/dev-app/chips/chips-demo.html
index 99726bb6fc22..9aecfb6ba882 100644
--- a/src/dev-app/chips/chips-demo.html
+++ b/src/dev-app/chips/chips-demo.html
@@ -160,6 +160,8 @@ <h4>Multi selection</h4>
 
       <mat-checkbox [(ngModel)]="disableInputs">Disabled</mat-checkbox>
       <mat-checkbox [(ngModel)]="editable">Editable</mat-checkbox>
+      <mat-checkbox [(ngModel)]="peopleWithAvatar">Show Avatar</mat-checkbox>
+      <mat-checkbox [(ngModel)]="showEditIcon">Show Edit Icon</mat-checkbox>
       <mat-checkbox [(ngModel)]="disabledInteractive">Disabled Interactive</mat-checkbox>
 
       <h4>Input is last child of chip grid</h4>
@@ -172,6 +174,14 @@ <h4>Input is last child of chip grid</h4>
               [editable]="editable"
               (removed)="remove(person)"
               (edited)="edit(person, $event)">
+              @if (showEditIcon) {
+                <button matChipEdit aria-label="Edit contributor">
+                  <mat-icon>edit</mat-icon>
+                </button>
+              }
+              @if (peopleWithAvatar && person.avatar) {
+                <mat-chip-avatar>{{person.avatar}}</mat-chip-avatar>
+              }
               {{person.name}}
               <button matChipRemove aria-label="Remove contributor">
                 <mat-icon>close</mat-icon>
diff --git a/src/dev-app/chips/chips-demo.ts b/src/dev-app/chips/chips-demo.ts
index 13dd012ed549..740bee4b97c1 100644
--- a/src/dev-app/chips/chips-demo.ts
+++ b/src/dev-app/chips/chips-demo.ts
@@ -20,6 +20,7 @@ import {MatIconModule} from '@angular/material/icon';
 
 export interface Person {
   name: string;
+  avatar?: string;
 }
 
 export interface DemoColor {
@@ -52,6 +53,8 @@ export class ChipsDemo {
   listboxesWithAvatar = false;
   disableInputs = false;
   editable = false;
+  peopleWithAvatar = false;
+  showEditIcon = false;
   disabledInteractive = false;
   message = '';
 
@@ -75,12 +78,12 @@ export class ChipsDemo {
   selectedPeople = null;
 
   people: Person[] = [
-    {name: 'Kara'},
-    {name: 'Jeremy'},
-    {name: 'Topher'},
-    {name: 'Elad'},
-    {name: 'Kristiyan'},
-    {name: 'Paul'},
+    {name: 'Kara', avatar: 'K'},
+    {name: 'Jeremy', avatar: 'J'},
+    {name: 'Topher', avatar: 'T'},
+    {name: 'Elad', avatar: 'E'},
+    {name: 'Kristiyan', avatar: 'K'},
+    {name: 'Paul', avatar: 'P'},
   ];
 
   availableColors: DemoColor[] = [
diff --git a/src/material/chips/chip-action.ts b/src/material/chips/chip-action.ts
index af665cf6cc64..432f75f97951 100644
--- a/src/material/chips/chip-action.ts
+++ b/src/material/chips/chip-action.ts
@@ -29,7 +29,8 @@ import {_StructuralStylesLoader} from '../core';
     'class': 'mdc-evolution-chip__action mat-mdc-chip-action',
     '[class.mdc-evolution-chip__action--primary]': '_isPrimary',
     '[class.mdc-evolution-chip__action--presentational]': '!isInteractive',
-    '[class.mdc-evolution-chip__action--trailing]': '!_isPrimary',
+    '[class.mdc-evolution-chip__action--secondary]': '!_isPrimary',
+    '[class.mdc-evolution-chip__action--trailing]': '!_isPrimary && !_isLeading',
     '[attr.tabindex]': '_getTabindex()',
     '[attr.disabled]': '_getDisabledAttribute()',
     '[attr.aria-disabled]': 'disabled',
@@ -43,6 +44,7 @@ export class MatChipAction {
     _handlePrimaryActionInteraction(): void;
     remove(): void;
     disabled: boolean;
+    _edit(): void;
     _isEditing?: boolean;
   }>(MAT_CHIP);
 
@@ -52,6 +54,9 @@ export class MatChipAction {
   /** Whether this is the primary action in the chip. */
   _isPrimary = true;
 
+  /** Whether this is the leading action in the chip. */
+  _isLeading = false; // TODO(adolgachev): consolidate usage to secondary css class
+
   /** Whether the action is disabled. */
   @Input({transform: booleanAttribute})
   get disabled(): boolean {
diff --git a/src/material/chips/chip-grid.spec.ts b/src/material/chips/chip-grid.spec.ts
index b3ca890d877d..385dca10c968 100644
--- a/src/material/chips/chip-grid.spec.ts
+++ b/src/material/chips/chip-grid.spec.ts
@@ -270,7 +270,7 @@ describe('MatChipGrid', () => {
           fixture = createComponent(ChipGridWithRemove);
           flush();
           trailingActions = chipGridNativeElement.querySelectorAll(
-            '.mdc-evolution-chip__action--trailing',
+            '.mdc-evolution-chip__action--secondary',
           );
         }));
 
@@ -595,7 +595,7 @@ describe('MatChipGrid', () => {
       const fixture = createComponent(ChipGridWithRemove, undefined, [NoopAnimationsModule]);
       flush();
       const trailingActions = chipGridNativeElement.querySelectorAll<HTMLElement>(
-        '.mdc-evolution-chip__action--trailing',
+        '.mdc-evolution-chip__action--secondary',
       );
       const chip = chips.get(2)!;
       chip.focus();
diff --git a/src/material/chips/chip-icons.ts b/src/material/chips/chip-icons.ts
index 46ab0f2dfe21..bc0a71b9d436 100644
--- a/src/material/chips/chip-icons.ts
+++ b/src/material/chips/chip-icons.ts
@@ -9,7 +9,7 @@
 import {ENTER, SPACE} from '@angular/cdk/keycodes';
 import {Directive} from '@angular/core';
 import {MatChipAction} from './chip-action';
-import {MAT_CHIP_AVATAR, MAT_CHIP_REMOVE, MAT_CHIP_TRAILING_ICON} from './tokens';
+import {MAT_CHIP_AVATAR, MAT_CHIP_EDIT, MAT_CHIP_REMOVE, MAT_CHIP_TRAILING_ICON} from './tokens';
 
 /** Avatar image within a chip. */
 @Directive({
@@ -42,6 +42,56 @@ export class MatChipTrailingIcon extends MatChipAction {
   override _isPrimary = false;
 }
 
+/**
+ * Directive to edit the parent chip when the leading action icon is clicked or
+ * when the ENTER key is pressed on it.
+ *
+ * Recommended for use with the Material Design "edit" icon
+ * available at https://material.io/icons/#ic_edit.
+ *
+ * Example:
+ *
+ * ```
+ * <mat-chip>
+ *   <button matChipEdit aria-label="Edit">
+ *     <mat-icon>edit</mat-icon>
+ *   </button>
+ * </mat-chip>
+ * ```
+ */
+
+@Directive({
+  selector: '[matChipEdit]',
+  host: {
+    'class':
+      'mat-mdc-chip-edit mat-mdc-chip-avatar mat-focus-indicator ' +
+      'mdc-evolution-chip__icon mdc-evolution-chip__icon--primary',
+    'role': 'button',
+    '[attr.aria-hidden]': 'null',
+  },
+  providers: [{provide: MAT_CHIP_EDIT, useExisting: MatChipEdit}],
+})
+export class MatChipEdit extends MatChipAction {
+  override _isPrimary = false;
+  override _isLeading = true;
+
+  override _handleClick(event: MouseEvent): void {
+    if (!this.disabled) {
+      event.stopPropagation();
+      event.preventDefault();
+      this._parentChip._edit();
+    }
+  }
+
+  override _handleKeydown(event: KeyboardEvent) {
+    if ((event.keyCode === ENTER || event.keyCode === SPACE) && !this.disabled) {
+      event.stopPropagation();
+      event.preventDefault();
+      this._parentChip._edit();
+    }
+  }
+}
+
 /**
  * Directive to remove the parent chip when the trailing icon is clicked or
  * when the ENTER key is pressed on it.
diff --git a/src/material/chips/chip-row.html b/src/material/chips/chip-row.html
index f2f5b0af5190..47f53c59b303 100644
--- a/src/material/chips/chip-row.html
+++ b/src/material/chips/chip-row.html
@@ -2,6 +2,11 @@
   <span class="mat-mdc-chip-focus-overlay"></span>
 }
 
+@if (_hasLeadingActionIcon()) {
+  <span class="mdc-evolution-chip__cell mdc-evolution-chip__cell--leading" role="gridcell">
+    <ng-content select="[matChipEdit]"></ng-content>
+  </span>
+}
 <span class="mdc-evolution-chip__cell mdc-evolution-chip__cell--primary" role="gridcell"
     matChipAction
     [disabled]="disabled"
diff --git a/src/material/chips/chip-row.spec.ts b/src/material/chips/chip-row.spec.ts
index b58a4f65a0d6..229692a2843d 100644
--- a/src/material/chips/chip-row.spec.ts
+++ b/src/material/chips/chip-row.spec.ts
@@ -436,6 +436,21 @@ describe('Row Chips', () => {
       }));
     });
 
+    describe('with edit icon', () => {
+      beforeEach(async () => {
+        testComponent.showEditIcon = true;
+        fixture.changeDetectorRef.markForCheck();
+        fixture.detectChanges();
+      });
+
+      it('should begin editing on edit click', () => {
+        expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeFalsy();
+        dispatchFakeEvent(chipNativeElement.querySelector('.mat-mdc-chip-edit')!, 'click');
+        fixture.detectChanges();
+        expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeTruthy();
+      });
+    });
+
     describe('a11y', () => {
       it('should apply `ariaLabel` and `ariaDesciption` to the primary gridcell', () => {
         fixture.componentInstance.ariaLabel = 'chip name';
@@ -488,6 +503,9 @@ describe('Row Chips', () => {
                   (destroyed)="chipDestroy($event)"
                   (removed)="chipRemove($event)" (edited)="chipEdit($event)"
                   [aria-label]="ariaLabel" [aria-description]="ariaDescription">
+            @if (showEditIcon) {
+              <button matChipEdit>edit</button>
+            }
             {{name}}
             <button matChipRemove>x</button>
             @if (useCustomEditInput) {
@@ -509,6 +527,7 @@ class SingleChip {
   removable: boolean = true;
   shouldShow: boolean = true;
   editable: boolean = false;
+  showEditIcon: boolean = false;
   useCustomEditInput: boolean = true;
   ariaLabel: string | null = null;
   ariaDescription: string | null = null;
diff --git a/src/material/chips/chip-row.ts b/src/material/chips/chip-row.ts
index a6b478664226..f7db3d950ff2 100644
--- a/src/material/chips/chip-row.ts
+++ b/src/material/chips/chip-row.ts
@@ -46,6 +46,7 @@ export interface MatChipEditedEvent extends MatChipEvent {
     '[class.mat-mdc-chip-editing]': '_isEditing',
     '[class.mat-mdc-chip-editable]': 'editable',
     '[class.mdc-evolution-chip--disabled]': 'disabled',
+    '[class.mdc-evolution-chip--with-leading-action]': '_hasLeadingActionIcon()',
     '[class.mdc-evolution-chip--with-trailing-action]': '_hasTrailingIcon()',
     '[class.mdc-evolution-chip--with-primary-graphic]': 'leadingIcon',
     '[class.mdc-evolution-chip--with-primary-icon]': 'leadingIcon',
@@ -130,6 +131,11 @@ export class MatChipRow extends MatChip implements AfterViewInit {
     });
   }
 
+  protected _hasLeadingActionIcon() {
+    // The leading action (edit) icon is hidden while editing.
+    return !this._isEditing && !!this.editIcon;
+  }
+
   override _hasTrailingIcon() {
     // The trailing icon is hidden while editing.
     return !this._isEditing && super._hasTrailingIcon();
@@ -174,10 +180,18 @@ export class MatChipRow extends MatChip implements AfterViewInit {
     }
   }
 
-  private _startEditing(event: Event) {
+  override _edit(): void {
+    // markForCheck necessary for edit input to be rendered
+    this._changeDetectorRef.markForCheck();
+    this._startEditing();
+  }
+
+  private _startEditing(event?: Event) {
     if (
       !this.primaryAction ||
-      (this.removeIcon && this._getSourceAction(event.target as Node) === this.removeIcon)
+      (this.removeIcon &&
+        !!event &&
+        this._getSourceAction(event.target as Node) === this.removeIcon)
     ) {
       return;
     }
@@ -191,7 +205,9 @@ export class MatChipRow extends MatChip implements AfterViewInit {
     afterNextRender(
       () => {
         this._getEditInput().initialize(value);
-        this._editStartPending = false;
+
+        // Necessary when using edit icon to prevent edit from aborting
+        setTimeout(() => this._ngZone.run(() => (this._editStartPending = false)));
       },
       {injector: this._injector},
     );
diff --git a/src/material/chips/chip.scss b/src/material/chips/chip.scss
index c0ae4c2d4407..27b9640c9dd1 100644
--- a/src/material/chips/chip.scss
+++ b/src/material/chips/chip.scss
@@ -130,8 +130,19 @@ $fallbacks: m3-chip.get-tokens();
 
   // Moved out into variables, because the selectors are too long.
   $with-graphic: '.mdc-evolution-chip--with-primary-graphic';
+  $with-leading: '.mdc-evolution-chip--with-leading-action';
   $with-trailing: '.mdc-evolution-chip--with-trailing-action';
 
+  .mat-mdc-standard-chip#{$with-leading} & {
+    padding-left: 0;
+    padding-right: $_action-padding;
+  }
+
+  [dir='rtl'] .mat-mdc-standard-chip#{$with-leading} & {
+    padding-left: $_action-padding;
+    padding-right: 0;
+  }
+
   .mat-mdc-standard-chip#{$with-trailing} & {
     padding-left: $_action-padding;
     padding-right: 0;
@@ -142,6 +153,11 @@ $fallbacks: m3-chip.get-tokens();
     padding-right: $_action-padding;
   }
 
+  .mat-mdc-standard-chip#{$with-leading}#{$with-trailing} & {
+    padding-left: 0;
+    padding-right: 0;
+  }
+
   .mat-mdc-standard-chip#{$with-graphic}#{$with-trailing} & {
     padding-left: 0;
     padding-right: 0;
@@ -173,7 +189,7 @@ $fallbacks: m3-chip.get-tokens();
   }
 }
 
-.mdc-evolution-chip__action--trailing {
+.mdc-evolution-chip__action--secondary {
   position: relative;
   overflow: visible;
 
@@ -199,7 +215,6 @@ $fallbacks: m3-chip.get-tokens();
     padding-right: $_trailing-action-padding;
   }
 
-
   .mdc-evolution-chip--with-avatar#{$with-graphic}#{$with-trailing} & {
     padding-left: $_avatar-trailing-padding;
     padding-right: $_avatar-trailing-padding;
@@ -262,6 +277,7 @@ $fallbacks: m3-chip.get-tokens();
   // Moved out into variables, because the selectors are too long.
   $with-icon: '.mdc-evolution-chip--with-primary-icon';
   $with-graphic: '.mdc-evolution-chip--with-primary-graphic';
+  $with-leading: '.mdc-evolution-chip--with-leading-action';
   $with-trailing: '.mdc-evolution-chip--with-trailing-action';
 
   .mdc-evolution-chip--selectable:not(.mdc-evolution-chip--selected):not(#{$with-icon}) & {
@@ -297,6 +313,10 @@ $fallbacks: m3-chip.get-tokens();
     padding-left: $_avatar-trailing-padding;
     padding-right: $_avatar-leading-padding;
   }
+
+  .mdc-evolution-chip--with-avatar#{$with-graphic}#{$with-leading} & {
+    padding-left: 0;
+  }
 }
 
 .mdc-evolution-chip__checkmark {
@@ -499,7 +519,7 @@ $fallbacks: m3-chip.get-tokens();
   }
 }
 
-.mat-mdc-chip-remove {
+.mat-mdc-chip-edit, .mat-mdc-chip-remove {
   opacity: token-utils.slot(chip-trailing-action-opacity, $fallbacks);
 
   &:focus {
@@ -650,7 +670,7 @@ $fallbacks: m3-chip.get-tokens();
   }
 }
 
-.mat-mdc-chip-remove {
+.mat-mdc-chip-edit, .mat-mdc-chip-remove {
   &::before {
     $default-border-width: focus-indicators-private.$default-border-width;
     $offset: var(--mat-focus-indicator-border-width, #{$default-border-width});
@@ -714,6 +734,6 @@ $fallbacks: m3-chip.get-tokens();
 // Prevents icon from being cut off when text spacing is increased.
 // .mat-mdc-chip-remove selector necessary for remove button with icon.
 // Fixes b/250063405.
-.mdc-evolution-chip__icon, .mat-mdc-chip-remove .mat-icon {
+.mdc-evolution-chip__icon, .mat-mdc-chip-edit .mat-icon, .mat-mdc-chip-remove .mat-icon {
   min-height: fit-content;
 }
diff --git a/src/material/chips/chip.ts b/src/material/chips/chip.ts
index 2aa5a827a7f7..85e2cde30552 100644
--- a/src/material/chips/chip.ts
+++ b/src/material/chips/chip.ts
@@ -44,8 +44,14 @@ import {
 } from '../core';
 import {Subject, Subscription, merge} from 'rxjs';
 import {MatChipAction} from './chip-action';
-import {MatChipAvatar, MatChipRemove, MatChipTrailingIcon} from './chip-icons';
-import {MAT_CHIP, MAT_CHIP_AVATAR, MAT_CHIP_REMOVE, MAT_CHIP_TRAILING_ICON} from './tokens';
+import {MatChipAvatar, MatChipEdit, MatChipRemove, MatChipTrailingIcon} from './chip-icons';
+import {
+  MAT_CHIP,
+  MAT_CHIP_AVATAR,
+  MAT_CHIP_EDIT,
+  MAT_CHIP_REMOVE,
+  MAT_CHIP_TRAILING_ICON,
+} from './tokens';
 
 /** Represents an event fired on an individual `mat-chip`. */
 export interface MatChipEvent {
@@ -133,6 +139,10 @@ export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck
   @ContentChildren(MAT_CHIP_TRAILING_ICON, {descendants: true})
   protected _allTrailingIcons: QueryList<MatChipTrailingIcon>;
 
+  /** All edit icons present in the chip. */
+  @ContentChildren(MAT_CHIP_EDIT, {descendants: true})
+  protected _allEditIcons: QueryList<MatChipEdit>;
+
   /** All remove icons present in the chip. */
   @ContentChildren(MAT_CHIP_REMOVE, {descendants: true})
   protected _allRemoveIcons: QueryList<MatChipRemove>;
@@ -225,6 +235,9 @@ export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck
   /** The chip's leading icon. */
   @ContentChild(MAT_CHIP_AVATAR) leadingIcon: MatChipAvatar;
 
+  /** The chip's leading edit icon. */
+  @ContentChild(MAT_CHIP_EDIT) editIcon: MatChipEdit;
+
   /** The chip's trailing icon. */
   @ContentChild(MAT_CHIP_TRAILING_ICON) trailingIcon: MatChipTrailingIcon;
 
@@ -279,6 +292,7 @@ export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck
     this._actionChanges = merge(
       this._allLeadingIcons.changes,
       this._allTrailingIcons.changes,
+      this._allEditIcons.changes,
       this._allRemoveIcons.changes,
     ).subscribe(() => this._changeDetectorRef.markForCheck());
   }
@@ -358,6 +372,10 @@ export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck
   _getActions(): MatChipAction[] {
     const result: MatChipAction[] = [];
 
+    if (this.editIcon) {
+      result.push(this.editIcon);
+    }
+
     if (this.primaryAction) {
       result.push(this.primaryAction);
     }
@@ -378,6 +396,11 @@ export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck
     // Empty here, but is overwritten in child classes.
   }
 
+  /** Handles interactions with the edit action of the chip. */
+  _edit(event: Event) {
+    // Empty here, but is overwritten in child classes.
+  }
+
   /** Starts the focus monitoring process on the chip. */
   private _monitorFocus() {
     this._focusMonitor.monitor(this._elementRef, true).subscribe(origin => {
diff --git a/src/material/chips/module.ts b/src/material/chips/module.ts
index 36771d4f9a41..105e6e88d16e 100644
--- a/src/material/chips/module.ts
+++ b/src/material/chips/module.ts
@@ -13,7 +13,7 @@ import {MatChip} from './chip';
 import {MAT_CHIPS_DEFAULT_OPTIONS, MatChipsDefaultOptions} from './tokens';
 import {MatChipEditInput} from './chip-edit-input';
 import {MatChipGrid} from './chip-grid';
-import {MatChipAvatar, MatChipRemove, MatChipTrailingIcon} from './chip-icons';
+import {MatChipAvatar, MatChipEdit, MatChipRemove, MatChipTrailingIcon} from './chip-icons';
 import {MatChipInput} from './chip-input';
 import {MatChipListbox} from './chip-listbox';
 import {MatChipRow} from './chip-row';
@@ -24,6 +24,7 @@ import {MatChipAction} from './chip-action';
 const CHIP_DECLARATIONS = [
   MatChip,
   MatChipAvatar,
+  MatChipEdit,
   MatChipEditInput,
   MatChipGrid,
   MatChipInput,
diff --git a/src/material/chips/testing/chip-edit-harness.ts b/src/material/chips/testing/chip-edit-harness.ts
new file mode 100644
index 000000000000..dacebe95f5cd
--- /dev/null
+++ b/src/material/chips/testing/chip-edit-harness.ts
@@ -0,0 +1,37 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.dev/license
+ */
+
+import {
+  ComponentHarness,
+  ComponentHarnessConstructor,
+  HarnessPredicate,
+} from '@angular/cdk/testing';
+import {ChipEditHarnessFilters} from './chip-harness-filters';
+
+/** Harness for interacting with a standard Material chip edit button in tests. */
+export class MatChipEditHarness extends ComponentHarness {
+  static hostSelector = '.mat-mdc-chip-edit';
+
+  /**
+   * Gets a `HarnessPredicate` that can be used to search for a chip edit with specific
+   * attributes.
+   * @param options Options for filtering which input instances are considered a match.
+   * @return a `HarnessPredicate` configured with the given options.
+   */
+  static with<T extends MatChipEditHarness>(
+    this: ComponentHarnessConstructor<T>,
+    options: ChipEditHarnessFilters = {},
+  ): HarnessPredicate<T> {
+    return new HarnessPredicate(this, options);
+  }
+
+  /** Clicks the edit button. */
+  async click(): Promise<void> {
+    return (await this.host()).click();
+  }
+}
diff --git a/src/material/chips/testing/chip-harness-filters.ts b/src/material/chips/testing/chip-harness-filters.ts
index 27a95806ebe3..d42e3c6a803a 100644
--- a/src/material/chips/testing/chip-harness-filters.ts
+++ b/src/material/chips/testing/chip-harness-filters.ts
@@ -43,6 +43,8 @@ export interface ChipRowHarnessFilters extends ChipHarnessFilters {}
 
 export interface ChipSetHarnessFilters extends BaseHarnessFilters {}
 
+export interface ChipEditHarnessFilters extends BaseHarnessFilters {}
+
 export interface ChipRemoveHarnessFilters extends BaseHarnessFilters {}
 
 export interface ChipAvatarHarnessFilters extends BaseHarnessFilters {}
diff --git a/src/material/chips/testing/chip-harness.ts b/src/material/chips/testing/chip-harness.ts
index 36fa99e5dedd..4b7c5133c70f 100644
--- a/src/material/chips/testing/chip-harness.ts
+++ b/src/material/chips/testing/chip-harness.ts
@@ -15,9 +15,11 @@ import {
 import {MatChipAvatarHarness} from './chip-avatar-harness';
 import {
   ChipAvatarHarnessFilters,
+  ChipEditHarnessFilters,
   ChipHarnessFilters,
   ChipRemoveHarnessFilters,
 } from './chip-harness-filters';
+import {MatChipEditHarness} from './chip-edit-harness';
 import {MatChipRemoveHarness} from './chip-remove-harness';
 
 /** Harness for interacting with a mat-chip in tests. */
@@ -62,6 +64,14 @@ export class MatChipHarness extends ContentContainerComponentHarness {
     await hostEl.sendKeys(TestKey.DELETE);
   }
 
+  /**
+   * Gets the edit button inside of a chip.
+   * @param filter Optionally filters which chips are included.
+   */
+  async geEditButton(filter: ChipEditHarnessFilters = {}): Promise<MatChipEditHarness> {
+    return this.locatorFor(MatChipEditHarness.with(filter))();
+  }
+
   /**
    * Gets the remove button inside of a chip.
    * @param filter Optionally filters which chips are included.
diff --git a/src/material/chips/testing/public-api.ts b/src/material/chips/testing/public-api.ts
index 49398cf712f8..222ff8ce2414 100644
--- a/src/material/chips/testing/public-api.ts
+++ b/src/material/chips/testing/public-api.ts
@@ -7,6 +7,7 @@
  */
 
 export * from './chip-avatar-harness';
+export * from './chip-edit-harness';
 export * from './chip-harness';
 export * from './chip-harness-filters';
 export * from './chip-input-harness';
diff --git a/src/material/chips/tokens.ts b/src/material/chips/tokens.ts
index 9c90f0253a0d..a95ed2c0b132 100644
--- a/src/material/chips/tokens.ts
+++ b/src/material/chips/tokens.ts
@@ -46,6 +46,13 @@ export const MAT_CHIP_AVATAR = new InjectionToken('MatChipAvatar');
  */
 export const MAT_CHIP_TRAILING_ICON = new InjectionToken('MatChipTrailingIcon');
 
+/**
+ * Injection token that can be used to reference instances of `MatChipEdit`. It serves as
+ * alternative token to the actual `MatChipEdit` class which could cause unnecessary
+ * retention of the class and its directive metadata.
+ */
+export const MAT_CHIP_EDIT = new InjectionToken('MatChipEdit');
+
 /**
  * Injection token that can be used to reference instances of `MatChipRemove`. It serves as
  * alternative token to the actual `MatChipRemove` class which could cause unnecessary