Skip to content

Commit e4f7f34

Browse files
zsheikh-engcrisbeto
authored andcommitted
fix(material/chips): correct focus management on chip destruction (#33329)
* fix(material/chips): correct focus management on chip destruction Un-privatized _redirectDestroyedChipFocus to allow MatChipGrid to override the aggressive focus-stealing behavior with a silent updateActiveItem call, respecting downstream layout stability. Fixes #14345 * Update chip-grid.ts * Update chip-grid.spec.ts * Update chip.ts (cherry picked from commit e49f127)
1 parent 9ad7964 commit e4f7f34

4 files changed

Lines changed: 42 additions & 8 deletions

File tree

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,25 @@ describe('MatChipGrid', () => {
180180
expect(chipGridNativeElement.getAttribute('tabindex')).toBe('-1');
181181
});
182182

183+
184+
it('should clear the active item in key manager when the last focused chip is destroyed', fakeAsync(() => {
185+
const fixture = createComponent(StandardChipGrid);
186+
fixture.detectChanges();
187+
188+
// Focus a chip
189+
chips.first.focus();
190+
fixture.detectChanges();
191+
192+
// Remove ALL chips
193+
fixture.componentInstance.foods = []; // Clear the bound data array
194+
fixture.detectChanges();
195+
flush();
196+
197+
// Verify key manager is reset to prevent stale references
198+
expect(chipGridInstance._keyManager.activeItemIndex).toBe(-1);
199+
}));
200+
201+
183202
describe('on chip destroy', () => {
184203
it('should focus the next item', () => {
185204
const fixture = createComponent(StandardChipGrid);

src/material/chips/chip-grid.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,21 @@ export class MatChipGrid
492492
this.stateChanges.next();
493493
}
494494

495+
protected override _redirectDestroyedChipFocus() {
496+
if (this._lastDestroyedFocusedChipIndex === null) {
497+
return;
498+
}
499+
500+
super._redirectDestroyedChipFocus();
501+
502+
// If there are no chips left, or the set focuses the input,
503+
// clear the active item silently to prevent stale references.
504+
if (!this._chips.length ||
505+
(this._chips.length === 1 && this._chips.first.disabled)) {
506+
this._keyManager.updateActiveItem(-1);
507+
}
508+
}
509+
495510
_focusLastChip() {
496511
if (this._chips.length) {
497512
this._chips.last.focus();

src/material/chips/chip-set.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class MatChipSet implements AfterViewInit, OnDestroy {
5353
private _dir = inject(Directionality, {optional: true});
5454

5555
/** Index of the last destroyed chip that had focus. */
56-
private _lastDestroyedFocusedChipIndex: number | null = null;
56+
protected _lastDestroyedFocusedChipIndex: number | null = null;
5757

5858
/** Used to manage focus within the chip list. */
5959
protected _keyManager!: FocusKeyManager<MatChipAction>;
@@ -310,7 +310,7 @@ export class MatChipSet implements AfterViewInit, OnDestroy {
310310
* Finds the next appropriate chip to move focus to,
311311
* if the currently-focused chip is destroyed.
312312
*/
313-
private _redirectDestroyedChipFocus() {
313+
protected _redirectDestroyedChipFocus() {
314314
if (this._lastDestroyedFocusedChipIndex == null) {
315315
return;
316316
}

src/material/chips/chip.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -298,12 +298,12 @@ export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck
298298
}
299299

300300
ngOnDestroy() {
301-
this._focusMonitor.stopMonitoring(this._elementRef);
302-
this._rippleLoader?.destroyRipple(this._elementRef.nativeElement);
303-
this._actionChanges?.unsubscribe();
304-
this.destroyed.emit({chip: this});
305-
this.destroyed.complete();
306-
}
301+
this.destroyed.emit({chip: this});
302+
this.destroyed.complete();
303+
this._focusMonitor.stopMonitoring(this._elementRef);
304+
this._rippleLoader?.destroyRipple(this._elementRef.nativeElement);
305+
this._actionChanges?.unsubscribe();
306+
}
307307

308308
/**
309309
* Allows for programmatic removal of the chip.

0 commit comments

Comments
 (0)