Skip to content

feat(material/chips): add (optional) edit icon to input chips #31041

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 30, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions goldens/material/chips/index.api.md
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions goldens/material/chips/testing/index.api.md
Original file line number Diff line number Diff line change
@@ -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>;
Original file line number Diff line number Diff line change
@@ -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>
10 changes: 10 additions & 0 deletions src/dev-app/chips/chips-demo.html
Original file line number Diff line number Diff line change
@@ -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>
15 changes: 9 additions & 6 deletions src/dev-app/chips/chips-demo.ts
Original file line number Diff line number Diff line change
@@ -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[] = [
7 changes: 6 additions & 1 deletion src/material/chips/chip-action.ts
Original file line number Diff line number Diff line change
@@ -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 {
4 changes: 2 additions & 2 deletions src/material/chips/chip-grid.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
52 changes: 51 additions & 1 deletion src/material/chips/chip-icons.ts
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions src/material/chips/chip-row.html
Original file line number Diff line number Diff line change
@@ -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"
19 changes: 19 additions & 0 deletions src/material/chips/chip-row.spec.ts
Original file line number Diff line number Diff line change
@@ -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;
22 changes: 19 additions & 3 deletions src/material/chips/chip-row.ts
Original file line number Diff line number Diff line change
@@ -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},
);
30 changes: 25 additions & 5 deletions src/material/chips/chip.scss
Original file line number Diff line number Diff line change
@@ -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;
}
27 changes: 25 additions & 2 deletions src/material/chips/chip.ts
Original file line number Diff line number Diff line change
@@ -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 => {
3 changes: 2 additions & 1 deletion src/material/chips/module.ts
Original file line number Diff line number Diff line change
@@ -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,
37 changes: 37 additions & 0 deletions src/material/chips/testing/chip-edit-harness.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
2 changes: 2 additions & 0 deletions src/material/chips/testing/chip-harness-filters.ts
Original file line number Diff line number Diff line change
@@ -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 {}
10 changes: 10 additions & 0 deletions src/material/chips/testing/chip-harness.ts
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions src/material/chips/testing/public-api.ts
Original file line number Diff line number Diff line change
@@ -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';
7 changes: 7 additions & 0 deletions src/material/chips/tokens.ts
Original file line number Diff line number Diff line change
@@ -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