Skip to content

Commit 46bcc21

Browse files
authored
fix(material/chips): simplify repeat chip removal prevention (#29048)
We have some logic that prevents chip removal if the users is holding down the backspace key. It currently breaks down in the case where a value is pasted into the input via click. These changes simplify our detection by using `KeyboardEvent.repeat` which also resolves the paste issue.
1 parent ce87e55 commit 46bcc21

File tree

5 files changed

+42
-67
lines changed

5 files changed

+42
-67
lines changed

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

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {animate, style, transition, trigger} from '@angular/animations';
22
import {Direction, Directionality} from '@angular/cdk/bidi';
33
import {
4-
A,
54
BACKSPACE,
65
DELETE,
76
END,
@@ -13,6 +12,8 @@ import {
1312
TAB,
1413
} from '@angular/cdk/keycodes';
1514
import {
15+
createKeyboardEvent,
16+
dispatchEvent,
1617
dispatchFakeEvent,
1718
dispatchKeyboardEvent,
1819
patchElementFocus,
@@ -789,28 +790,15 @@ describe('MDC-based MatChipGrid', () => {
789790
expectLastCellFocused();
790791
});
791792

792-
it(
793-
'should not focus the last chip when pressing BACKSPACE after changing input, ' +
794-
'until BACKSPACE is released and pressed again',
795-
() => {
796-
// Change the input
797-
dispatchKeyboardEvent(nativeInput, 'keydown', A);
798-
799-
// It shouldn't focus until backspace is released and pressed again
800-
dispatchKeyboardEvent(nativeInput, 'keydown', BACKSPACE);
801-
dispatchKeyboardEvent(nativeInput, 'keydown', BACKSPACE);
802-
dispatchKeyboardEvent(nativeInput, 'keydown', BACKSPACE);
803-
expectNoCellFocused();
804-
805-
// Still not focused
806-
dispatchKeyboardEvent(nativeInput, 'keyup', BACKSPACE);
807-
expectNoCellFocused();
808-
809-
// Only now should it focus the last element
810-
dispatchKeyboardEvent(nativeInput, 'keydown', BACKSPACE);
811-
expectLastCellFocused();
812-
},
813-
);
793+
it('should not focus the last chip when the BACKSPACE key is being repeated', () => {
794+
// Only now should it focus the last element
795+
const event = createKeyboardEvent('keydown', BACKSPACE);
796+
Object.defineProperty(event, 'repeat', {
797+
get: () => true,
798+
});
799+
dispatchEvent(nativeInput, event);
800+
expectNoCellFocused();
801+
});
814802

815803
it('should focus last chip after pressing BACKSPACE after creating a chip', () => {
816804
// Create a chip

src/material/chips/chip-input.ts

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
import {BACKSPACE, hasModifierKey} from '@angular/cdk/keycodes';
1010
import {
11-
AfterContentInit,
1211
Directive,
1312
ElementRef,
1413
EventEmitter,
@@ -57,7 +56,6 @@ let nextUniqueId = 0;
5756
// the MDC chips were landed initially with it.
5857
'class': 'mat-mdc-chip-input mat-mdc-input-element mdc-text-field__input mat-input-element',
5958
'(keydown)': '_keydown($event)',
60-
'(keyup)': '_keyup($event)',
6159
'(blur)': '_blur()',
6260
'(focus)': '_focus()',
6361
'(input)': '_onInput()',
@@ -70,10 +68,7 @@ let nextUniqueId = 0;
7068
},
7169
standalone: true,
7270
})
73-
export class MatChipInput implements MatChipTextControl, AfterContentInit, OnChanges, OnDestroy {
74-
/** Used to prevent focus moving to chips while user is holding backspace */
75-
private _focusLastChipOnBackspace: boolean;
76-
71+
export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy {
7772
/** Whether the control is focused. */
7873
focused: boolean = false;
7974

@@ -153,36 +148,17 @@ export class MatChipInput implements MatChipTextControl, AfterContentInit, OnCha
153148
this.chipEnd.complete();
154149
}
155150

156-
ngAfterContentInit(): void {
157-
this._focusLastChipOnBackspace = this.empty;
158-
}
159-
160151
/** Utility method to make host definition/tests more clear. */
161-
_keydown(event?: KeyboardEvent) {
162-
if (event) {
163-
// To prevent the user from accidentally deleting chips when pressing BACKSPACE continuously,
164-
// We focus the last chip on backspace only after the user has released the backspace button,
165-
// And the input is empty (see behaviour in _keyup)
166-
if (event.keyCode === BACKSPACE && this._focusLastChipOnBackspace) {
152+
_keydown(event: KeyboardEvent) {
153+
if (this.empty && event.keyCode === BACKSPACE) {
154+
// Ignore events where the user is holding down backspace
155+
// so that we don't accidentally remove too many chips.
156+
if (!event.repeat) {
167157
this._chipGrid._focusLastChip();
168-
event.preventDefault();
169-
return;
170-
} else {
171-
this._focusLastChipOnBackspace = false;
172158
}
173-
}
174-
175-
this._emitChipEnd(event);
176-
}
177-
178-
/**
179-
* Pass events to the keyboard manager. Available here for tests.
180-
*/
181-
_keyup(event: KeyboardEvent) {
182-
// Allow user to move focus to chips next time he presses backspace
183-
if (!this._focusLastChipOnBackspace && event.keyCode === BACKSPACE && this.empty) {
184-
this._focusLastChipOnBackspace = true;
185159
event.preventDefault();
160+
} else {
161+
this._emitChipEnd(event);
186162
}
187163
}
188164

@@ -201,7 +177,6 @@ export class MatChipInput implements MatChipTextControl, AfterContentInit, OnCha
201177

202178
_focus() {
203179
this.focused = true;
204-
this._focusLastChipOnBackspace = this.empty;
205180
this._chipGrid.stateChanges.next();
206181
}
207182

@@ -231,7 +206,6 @@ export class MatChipInput implements MatChipTextControl, AfterContentInit, OnCha
231206
/** Clears the input */
232207
clear(): void {
233208
this.inputElement.value = '';
234-
this._focusLastChipOnBackspace = true;
235209
}
236210

237211
setDescribedByIds(ids: string[]): void {

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ describe('MDC-based Row Chips', () => {
123123

124124
spyOn(testComponent, 'chipRemove');
125125

126-
chipInstance._handleKeydown(DELETE_EVENT);
126+
dispatchEvent(chipNativeElement, DELETE_EVENT);
127127
fixture.detectChanges();
128128

129129
expect(testComponent.chipRemove).toHaveBeenCalled();
@@ -134,11 +134,25 @@ describe('MDC-based Row Chips', () => {
134134

135135
spyOn(testComponent, 'chipRemove');
136136

137-
chipInstance._handleKeydown(BACKSPACE_EVENT);
137+
dispatchEvent(chipNativeElement, BACKSPACE_EVENT);
138138
fixture.detectChanges();
139139

140140
expect(testComponent.chipRemove).toHaveBeenCalled();
141141
});
142+
143+
it('should not remove for repeated BACKSPACE event', () => {
144+
const BACKSPACE_EVENT = createKeyboardEvent('keydown', BACKSPACE);
145+
Object.defineProperty(BACKSPACE_EVENT, 'repeat', {
146+
get: () => true,
147+
});
148+
149+
spyOn(testComponent, 'chipRemove');
150+
151+
dispatchEvent(chipNativeElement, BACKSPACE_EVENT);
152+
fixture.detectChanges();
153+
154+
expect(testComponent.chipRemove).not.toHaveBeenCalled();
155+
});
142156
});
143157

144158
describe('when removable is false', () => {
@@ -152,7 +166,7 @@ describe('MDC-based Row Chips', () => {
152166

153167
spyOn(testComponent, 'chipRemove');
154168

155-
chipInstance._handleKeydown(DELETE_EVENT);
169+
dispatchEvent(chipNativeElement, DELETE_EVENT);
156170
fixture.detectChanges();
157171

158172
expect(testComponent.chipRemove).not.toHaveBeenCalled();
@@ -164,7 +178,7 @@ describe('MDC-based Row Chips', () => {
164178
spyOn(testComponent, 'chipRemove');
165179

166180
// Use the delete to remove the chip
167-
chipInstance._handleKeydown(BACKSPACE_EVENT);
181+
dispatchEvent(chipNativeElement, BACKSPACE_EVENT);
168182
fixture.detectChanges();
169183

170184
expect(testComponent.chipRemove).not.toHaveBeenCalled();

src/material/chips/chip.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,9 @@ export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck
336336

337337
/** Handles keyboard events on the chip. */
338338
_handleKeydown(event: KeyboardEvent) {
339-
if (event.keyCode === BACKSPACE || event.keyCode === DELETE) {
339+
// Ignore backspace events where the user is holding down the key
340+
// so that we don't accidentally remove too many chips.
341+
if ((event.keyCode === BACKSPACE && !event.repeat) || event.keyCode === DELETE) {
340342
event.preventDefault();
341343
this.remove();
342344
}

tools/public_api_guard/material/chips.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ export class MatChipGridChange {
249249
}
250250

251251
// @public
252-
export class MatChipInput implements MatChipTextControl, AfterContentInit, OnChanges, OnDestroy {
252+
export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy {
253253
constructor(_elementRef: ElementRef<HTMLInputElement>, defaultOptions: MatChipsDefaultOptions, formField?: MatFormField);
254254
addOnBlur: boolean;
255255
_blur(): void;
@@ -269,15 +269,12 @@ export class MatChipInput implements MatChipTextControl, AfterContentInit, OnCha
269269
focused: boolean;
270270
id: string;
271271
readonly inputElement: HTMLInputElement;
272-
_keydown(event?: KeyboardEvent): void;
273-
_keyup(event: KeyboardEvent): void;
272+
_keydown(event: KeyboardEvent): void;
274273
// (undocumented)
275274
static ngAcceptInputType_addOnBlur: unknown;
276275
// (undocumented)
277276
static ngAcceptInputType_disabled: unknown;
278277
// (undocumented)
279-
ngAfterContentInit(): void;
280-
// (undocumented)
281278
ngOnChanges(): void;
282279
// (undocumented)
283280
ngOnDestroy(): void;

0 commit comments

Comments
 (0)