Skip to content

Commit c1543a3

Browse files
committed
feat(material/chips): prevent chips from being deleted when user holds backspace
1 parent adddf13 commit c1543a3

File tree

7 files changed

+92
-67
lines changed

7 files changed

+92
-67
lines changed

src/components-examples/material/chips/chips-autocomplete/chips-autocomplete-example.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,15 @@ export class ChipsAutocompleteExample {
3434
}
3535

3636
add(event: MatChipInputEvent): void {
37-
const input = event.input;
38-
const value = event.value;
37+
const value = (event.value || '').trim();
3938

4039
// Add our fruit
41-
if ((value || '').trim()) {
42-
this.fruits.push(value.trim());
40+
if (value) {
41+
this.fruits.push(value);
4342
}
4443

45-
// Reset the input value
46-
if (input) {
47-
input.value = '';
48-
}
44+
// Clear the input value
45+
event.clearInput();
4946

5047
this.fruitCtrl.setValue(null);
5148
}

src/components-examples/material/chips/chips-input/chips-input-example.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,15 @@ export class ChipsInputExample {
2727
];
2828

2929
add(event: MatChipInputEvent): void {
30-
const input = event.input;
31-
const value = event.value;
30+
const value = (event.value || '').trim();
3231

3332
// Add our fruit
34-
if ((value || '').trim()) {
35-
this.fruits.push({name: value.trim()});
33+
if (value) {
34+
this.fruits.push({name: value});
3635
}
3736

38-
// Reset the input value
39-
if (input) {
40-
input.value = '';
41-
}
37+
// Clear the input value
38+
event.clearInput();
4239
}
4340

4441
remove(fruit: Fruit): void {

src/dev-app/chips/chips-demo.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {Component} from '@angular/core';
1111
import {MatChipInputEvent} from '@angular/material/chips';
1212
import {ThemePalette} from '@angular/material/core';
1313

14-
1514
export interface Person {
1615
name: string;
1716
}
@@ -61,17 +60,15 @@ export class ChipsDemo {
6160
}
6261

6362
add(event: MatChipInputEvent): void {
64-
const {input, value} = event;
63+
const value = (event.value || '').trim();
6564

6665
// Add our person
67-
if ((value || '').trim()) {
68-
this.people.push({ name: value.trim() });
66+
if (value) {
67+
this.people.push({ name: value });
6968
}
7069

71-
// Reset the input value
72-
if (input) {
73-
input.value = '';
74-
}
70+
// Clear the input value
71+
event.clearInput();
7572
}
7673

7774
remove(person: Person): void {

src/material/chips/chip-input.ts

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,32 @@
77
*/
88

99
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
10-
import {hasModifierKey, TAB} from '@angular/cdk/keycodes';
11-
import {Directive, ElementRef, EventEmitter, Inject, Input, OnChanges, Output} from '@angular/core';
10+
import {BACKSPACE, hasModifierKey, TAB} from '@angular/cdk/keycodes';
11+
import {
12+
AfterContentInit,
13+
Directive,
14+
ElementRef,
15+
EventEmitter,
16+
Inject,
17+
Input,
18+
OnChanges,
19+
OnDestroy,
20+
Output
21+
} from '@angular/core';
1222
import {MatChipsDefaultOptions, MAT_CHIPS_DEFAULT_OPTIONS} from './chip-default-options';
1323
import {MatChipList} from './chip-list';
1424
import {MatChipTextControl} from './chip-text-control';
1525

16-
1726
/** Represents an input event on a `matChipInput`. */
1827
export interface MatChipInputEvent {
1928
/** The native `<input>` element that the event is being fired for. */
2029
input: HTMLInputElement;
2130

2231
/** The value of the input. */
2332
value: string;
33+
34+
/** Call to clear the value of the input */
35+
clearInput(): void;
2436
}
2537

2638
// Increasing integer for generating unique ids.
@@ -36,6 +48,7 @@ let nextUniqueId = 0;
3648
host: {
3749
'class': 'mat-chip-input mat-input-element',
3850
'(keydown)': '_keydown($event)',
51+
'(keyup)': '_keyup($event)',
3952
'(blur)': '_blur()',
4053
'(focus)': '_focus()',
4154
'(input)': '_onInput()',
@@ -46,7 +59,10 @@ let nextUniqueId = 0;
4659
'[attr.aria-required]': '_chipList && _chipList.required || null',
4760
}
4861
})
49-
export class MatChipInput implements MatChipTextControl, OnChanges {
62+
export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy, AfterContentInit {
63+
/** Used to prevent focus moving to chips while user is holding backspace */
64+
private _focusLastChipOnBackspace: boolean;
65+
5066
/** Whether the control is focused. */
5167
focused: boolean = false;
5268
_chipList: MatChipList;
@@ -105,21 +121,50 @@ export class MatChipInput implements MatChipTextControl, OnChanges {
105121
this._inputElement = this._elementRef.nativeElement as HTMLInputElement;
106122
}
107123

108-
ngOnChanges() {
124+
ngOnChanges(): void {
109125
this._chipList.stateChanges.next();
110126
}
111127

128+
ngOnDestroy(): void {
129+
this.chipEnd.complete();
130+
}
131+
132+
ngAfterContentInit(): void {
133+
this._focusLastChipOnBackspace = this.empty;
134+
}
135+
112136
/** Utility method to make host definition/tests more clear. */
113137
_keydown(event?: KeyboardEvent) {
114-
// Allow the user's focus to escape when they're tabbing forward. Note that we don't
115-
// want to do this when going backwards, because focus should go back to the first chip.
116-
if (event && event.keyCode === TAB && !hasModifierKey(event, 'shiftKey')) {
117-
this._chipList._allowFocusEscape();
138+
if (event) {
139+
// Allow the user's focus to escape when they're tabbing forward. Note that we don't
140+
// want to do this when going backwards, because focus should go back to the first chip.
141+
if (event.keyCode === TAB && !hasModifierKey(event, 'shiftKey')) {
142+
this._chipList._allowFocusEscape();
143+
}
144+
145+
if (event.keyCode === BACKSPACE && this._focusLastChipOnBackspace) {
146+
this._chipList._keyManager.setLastItemActive();
147+
event.preventDefault();
148+
return;
149+
} else {
150+
this._focusLastChipOnBackspace = false;
151+
}
118152
}
119153

120154
this._emitChipEnd(event);
121155
}
122156

157+
/**
158+
* Pass events to the keyboard manager. Available here for tests.
159+
*/
160+
_keyup(event: KeyboardEvent) {
161+
// Allow user to move focus to chips next time he presses backspace
162+
if (!this._focusLastChipOnBackspace && event.keyCode === BACKSPACE && this.empty) {
163+
this._focusLastChipOnBackspace = true;
164+
event.preventDefault();
165+
}
166+
}
167+
123168
/** Checks to see if the blur should emit the (chipEnd) event. */
124169
_blur() {
125170
if (this.addOnBlur) {
@@ -143,12 +188,18 @@ export class MatChipInput implements MatChipTextControl, OnChanges {
143188
if (!this._inputElement.value && !!event) {
144189
this._chipList._keydown(event);
145190
}
146-
if (!event || this._isSeparatorKey(event)) {
147-
this.chipEnd.emit({ input: this._inputElement, value: this._inputElement.value });
148191

149-
if (event) {
150-
event.preventDefault();
151-
}
192+
if (!event || this._isSeparatorKey(event)) {
193+
this.chipEnd.emit({
194+
input: this._inputElement,
195+
value: this._inputElement.value,
196+
clearInput: () => {
197+
this._inputElement.value = '';
198+
this._focusLastChipOnBackspace = true;
199+
}
200+
});
201+
202+
event?.preventDefault();
152203
}
153204
}
154205

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

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1516,21 +1516,18 @@ class InputChipList {
15161516
isRequired: boolean;
15171517

15181518
add(event: MatChipInputEvent): void {
1519-
let input = event.input;
1520-
let value = event.value;
1519+
const value = (event.value || '').trim();
15211520

15221521
// Add our foods
1523-
if ((value || '').trim()) {
1522+
if (value) {
15241523
this.foods.push({
1525-
value: `${value.trim().toLowerCase()}-${this.foods.length}`,
1526-
viewValue: value.trim()
1524+
value: `${value.toLowerCase()}-${this.foods.length}`,
1525+
viewValue: value
15271526
});
15281527
}
15291528

1530-
// Reset the input value
1531-
if (input) {
1532-
input.value = '';
1533-
}
1529+
// Clear the input value
1530+
event.clearInput();
15341531
}
15351532

15361533
remove(food: any): void {

src/material/chips/chip-list.ts

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {FocusKeyManager} from '@angular/cdk/a11y';
1010
import {Directionality} from '@angular/cdk/bidi';
1111
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
1212
import {SelectionModel} from '@angular/cdk/collections';
13-
import {BACKSPACE} from '@angular/cdk/keycodes';
1413
import {
1514
AfterContentInit,
1615
ChangeDetectionStrategy,
@@ -42,7 +41,6 @@ import {startWith, takeUntil} from 'rxjs/operators';
4241
import {MatChip, MatChipEvent, MatChipSelectionChange} from './chip';
4342
import {MatChipTextControl} from './chip-text-control';
4443

45-
4644
// Boilerplate for applying mixins to MatChipList.
4745
/** @docs-private */
4846
class MatChipListBase {
@@ -68,7 +66,6 @@ export class MatChipListChange {
6866
public value: any) { }
6967
}
7068

71-
7269
/**
7370
* A material design chips component (named ChipList for its similarity to the List component).
7471
*/
@@ -415,7 +412,6 @@ export class MatChipList extends _MatChipListMixinBase implements MatFormFieldCo
415412
this._dropSubscriptions();
416413
}
417414

418-
419415
/** Associates an HTML input element with this chip list. */
420416
registerInput(inputElement: MatChipTextControl): void {
421417
this._chipInput = inputElement;
@@ -499,17 +495,12 @@ export class MatChipList extends _MatChipListMixinBase implements MatFormFieldCo
499495
_keydown(event: KeyboardEvent) {
500496
const target = event.target as HTMLElement;
501497

502-
// If they are on an empty input and hit backspace, focus the last chip
503-
if (event.keyCode === BACKSPACE && this._isInputEmpty(target)) {
504-
this._keyManager.setLastItemActive();
505-
event.preventDefault();
506-
} else if (target && target.classList.contains('mat-chip')) {
498+
if (target && target.classList.contains('mat-chip')) {
507499
this._keyManager.onKeydown(event);
508500
this.stateChanges.next();
509501
}
510502
}
511503

512-
513504
/**
514505
* Check the tab index as you should not be allowed to focus an empty list.
515506
*/
@@ -546,15 +537,6 @@ export class MatChipList extends _MatChipListMixinBase implements MatFormFieldCo
546537
return index >= 0 && index < this.chips.length;
547538
}
548539

549-
private _isInputEmpty(element: HTMLElement): boolean {
550-
if (element && element.nodeName.toLowerCase() === 'input') {
551-
let input = element as HTMLInputElement;
552-
return !input.value;
553-
}
554-
555-
return false;
556-
}
557-
558540
_setSelectionByValue(value: any, isUserInput: boolean = true) {
559541
this._clearSelection();
560542
this.chips.forEach(chip => chip.deselect());

tools/public_api_guard/material/chips.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export interface MatChipEvent {
7070
chip: MatChip;
7171
}
7272

73-
export declare class MatChipInput implements MatChipTextControl, OnChanges {
73+
export declare class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy, AfterContentInit {
7474
_addOnBlur: boolean;
7575
_chipList: MatChipList;
7676
protected _elementRef: ElementRef<HTMLInputElement>;
@@ -91,9 +91,12 @@ export declare class MatChipInput implements MatChipTextControl, OnChanges {
9191
_emitChipEnd(event?: KeyboardEvent): void;
9292
_focus(): void;
9393
_keydown(event?: KeyboardEvent): void;
94+
_keyup(event: KeyboardEvent): void;
9495
_onInput(): void;
9596
focus(options?: FocusOptions): void;
97+
ngAfterContentInit(): void;
9698
ngOnChanges(): void;
99+
ngOnDestroy(): void;
97100
static ngAcceptInputType_addOnBlur: BooleanInput;
98101
static ngAcceptInputType_disabled: BooleanInput;
99102
static ɵdir: i0.ɵɵDirectiveDefWithMeta<MatChipInput, "input[matChipInputFor]", ["matChipInput", "matChipInputFor"], { "chipList": "matChipInputFor"; "addOnBlur": "matChipInputAddOnBlur"; "separatorKeyCodes": "matChipInputSeparatorKeyCodes"; "placeholder": "placeholder"; "id": "id"; "disabled": "disabled"; }, { "chipEnd": "matChipInputTokenEnd"; }, never>;
@@ -103,6 +106,7 @@ export declare class MatChipInput implements MatChipTextControl, OnChanges {
103106
export interface MatChipInputEvent {
104107
input: HTMLInputElement;
105108
value: string;
109+
clearInput(): void;
106110
}
107111

108112
export declare class MatChipList extends _MatChipListMixinBase implements MatFormFieldControl<any>, ControlValueAccessor, AfterContentInit, DoCheck, OnInit, OnDestroy, CanUpdateErrorState {

0 commit comments

Comments
 (0)