From 4381b8ef42e3adeb26fd148aecbfe640a56ab2ba Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Fri, 12 Mar 2021 10:16:37 -0800 Subject: [PATCH] Revert "feat(material/chips): prevent chips from being deleted when user holds backspace (#19700)" This reverts commit 623056083b21ebae25dcb4e0b9a83d6429b1d3b4. --- .../chips-autocomplete-example.ts | 13 +- .../chips/chips-input/chips-input-example.ts | 13 +- src/dev-app/chips/chips-demo.ts | 13 +- src/dev-app/mdc-chips/mdc-chips-demo.ts | 12 +- .../mdc-chips/chip-grid.spec.ts | 394 +++++++++--------- .../mdc-chips/chip-grid.ts | 30 +- .../mdc-chips/chip-input.ts | 100 +---- src/material/chips/chip-input.ts | 102 +---- src/material/chips/chip-list.spec.ts | 82 ++-- src/material/chips/chip-list.ts | 20 +- tools/public_api_guard/material/chips.d.ts | 9 +- 11 files changed, 332 insertions(+), 456 deletions(-) diff --git a/src/components-examples/material/chips/chips-autocomplete/chips-autocomplete-example.ts b/src/components-examples/material/chips/chips-autocomplete/chips-autocomplete-example.ts index 8939f31f5b31..9ebe89ec9ed0 100644 --- a/src/components-examples/material/chips/chips-autocomplete/chips-autocomplete-example.ts +++ b/src/components-examples/material/chips/chips-autocomplete/chips-autocomplete-example.ts @@ -34,15 +34,18 @@ export class ChipsAutocompleteExample { } add(event: MatChipInputEvent): void { - const value = (event.value || '').trim(); + const input = event.input; + const value = event.value; // Add our fruit - if (value) { - this.fruits.push(value); + if ((value || '').trim()) { + this.fruits.push(value.trim()); } - // Clear the input value - event.chipInput!.clear(); + // Reset the input value + if (input) { + input.value = ''; + } this.fruitCtrl.setValue(null); } diff --git a/src/components-examples/material/chips/chips-input/chips-input-example.ts b/src/components-examples/material/chips/chips-input/chips-input-example.ts index cec9e34f8348..eaedd5100652 100644 --- a/src/components-examples/material/chips/chips-input/chips-input-example.ts +++ b/src/components-examples/material/chips/chips-input/chips-input-example.ts @@ -27,15 +27,18 @@ export class ChipsInputExample { ]; add(event: MatChipInputEvent): void { - const value = (event.value || '').trim(); + const input = event.input; + const value = event.value; // Add our fruit - if (value) { - this.fruits.push({name: value}); + if ((value || '').trim()) { + this.fruits.push({name: value.trim()}); } - // Clear the input value - event.chipInput!.clear(); + // Reset the input value + if (input) { + input.value = ''; + } } remove(fruit: Fruit): void { diff --git a/src/dev-app/chips/chips-demo.ts b/src/dev-app/chips/chips-demo.ts index a4ee39c19988..0702a41aea22 100644 --- a/src/dev-app/chips/chips-demo.ts +++ b/src/dev-app/chips/chips-demo.ts @@ -11,6 +11,7 @@ import {Component} from '@angular/core'; import {MatChipInputEvent} from '@angular/material/chips'; import {ThemePalette} from '@angular/material/core'; + export interface Person { name: string; } @@ -60,15 +61,17 @@ export class ChipsDemo { } add(event: MatChipInputEvent): void { - const value = (event.value || '').trim(); + const {input, value} = event; // Add our person - if (value) { - this.people.push({ name: value }); + if ((value || '').trim()) { + this.people.push({ name: value.trim() }); } - // Clear the input value - event.chipInput!.clear(); + // Reset the input value + if (input) { + input.value = ''; + } } remove(person: Person): void { diff --git a/src/dev-app/mdc-chips/mdc-chips-demo.ts b/src/dev-app/mdc-chips/mdc-chips-demo.ts index 24639042c4fc..c93da13d7bd7 100644 --- a/src/dev-app/mdc-chips/mdc-chips-demo.ts +++ b/src/dev-app/mdc-chips/mdc-chips-demo.ts @@ -61,15 +61,17 @@ export class MdcChipsDemo { } add(event: MatChipInputEvent): void { - const value = (event.value || '').trim(); + const {input, value} = event; // Add our person - if (value) { - this.people.push({ name: value }); + if ((value || '').trim()) { + this.people.push({ name: value.trim() }); } - // Clear the input value - event.chipInput!.clear(); + // Reset the input value + if (input) { + input.value = ''; + } } remove(person: Person): void { diff --git a/src/material-experimental/mdc-chips/chip-grid.spec.ts b/src/material-experimental/mdc-chips/chip-grid.spec.ts index ab29237b694f..7488bb090da3 100644 --- a/src/material-experimental/mdc-chips/chip-grid.spec.ts +++ b/src/material-experimental/mdc-chips/chip-grid.spec.ts @@ -1,7 +1,6 @@ import {animate, style, transition, trigger} from '@angular/animations'; import {Direction, Directionality} from '@angular/cdk/bidi'; import { - A, BACKSPACE, DELETE, END, @@ -50,6 +49,7 @@ import { describe('MDC-based MatChipGrid', () => { + let fixture: ComponentFixture; let chipGridDebugElement: DebugElement; let chipGridNativeElement: HTMLElement; let chipGridInstance: MatChipGrid; @@ -59,22 +59,10 @@ describe('MDC-based MatChipGrid', () => { let testComponent: StandardChipGrid; let dirChange: Subject; - const expectNoCellFocused = () => { - expect(manager.activeRowIndex).toBe(-1); - expect(manager.activeColumnIndex).toBe(-1); - }; - - const expectLastCellFocused = () => { - expect(manager.activeRowIndex).toBe(chips.length - 1); - expect(manager.activeColumnIndex).toBe(0); - }; - describe('StandardChipGrid', () => { describe('basic behaviors', () => { - let fixture: ComponentFixture; - beforeEach(() => { - fixture = setupStandardGrid(); + setupStandardGrid(); }); it('should add the `mat-mdc-chip-set` class', () => { @@ -120,11 +108,9 @@ describe('MDC-based MatChipGrid', () => { }); describe('focus behaviors', () => { - let fixture: ComponentFixture | - ComponentFixture; - beforeEach(() => { - fixture = setupStandardGrid(); + setupStandardGrid(); + manager = chipGridInstance._keyManager; }); it('should focus the first chip on focus', () => { @@ -136,9 +122,11 @@ describe('MDC-based MatChipGrid', () => { }); it('should watch for chip focus', () => { - const lastIndex = chips.length - 1; + let array = chips.toArray(); + let lastIndex = array.length - 1; + let lastItem = array[lastIndex]; - chips.last.focus(); + lastItem.focus(); fixture.detectChanges(); expect(manager.activeRowIndex).toBe(lastIndex); @@ -167,7 +155,8 @@ describe('MDC-based MatChipGrid', () => { describe('on chip destroy', () => { it('should focus the next item', () => { - const midItem = chips.get(2)!; + let array = chips.toArray(); + let midItem = array[2]; // Focus the middle item midItem.focus(); @@ -181,10 +170,12 @@ describe('MDC-based MatChipGrid', () => { }); it('should focus the previous item', () => { - const lastIndex = chips.length - 1; + let array = chips.toArray(); + let lastIndex = array.length - 1; + let lastItem = array[lastIndex]; // Focus the last item - chips.last.focus(); + lastItem.focus(); // Destroy the last item testComponent.chips.pop(); @@ -195,7 +186,8 @@ describe('MDC-based MatChipGrid', () => { }); it('should not focus if chip grid is not focused', fakeAsync(() => { - const midItem = chips.get(2)!; + let array = chips.toArray(); + let midItem = array[2]; // Focus and blur the middle item midItem.focus(); @@ -227,8 +219,14 @@ describe('MDC-based MatChipGrid', () => { 'component with animations', fakeAsync(() => { fixture.destroy(); TestBed.resetTestingModule(); - fixture = createComponent(StandardChipGridWithAnimations, [], BrowserAnimationsModule); + fixture.detectChanges(); + + chipGridDebugElement = fixture.debugElement.query(By.directive(MatChipGrid))!; + chipGridNativeElement = chipGridDebugElement.nativeElement; + chipGridInstance = chipGridDebugElement.componentInstance; + testComponent = fixture.debugElement.componentInstance; + chips = chipGridInstance._chips; chips.last.focus(); fixture.detectChanges(); @@ -254,23 +252,30 @@ describe('MDC-based MatChipGrid', () => { }); describe('keyboard behavior', () => { - describe('LTR (default)', () => { - let fixture: ComponentFixture; - beforeEach(() => { fixture = createComponent(ChipGridWithRemove); + fixture.detectChanges(); + + chipGridDebugElement = fixture.debugElement.query(By.directive(MatChipGrid))!; + chipGridInstance = chipGridDebugElement.componentInstance; + chipGridNativeElement = chipGridDebugElement.nativeElement; + chips = chipGridInstance._chips; + manager = chipGridInstance._keyManager; }); it('should focus previous column when press LEFT ARROW', () => { let nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row'); let lastNativeChip = nativeChips[nativeChips.length - 1] as HTMLElement; - const lastRowIndex = chips.length - 1; + let array = chips.toArray(); + let lastRowIndex = array.length - 1; + let lastChip = array[lastRowIndex]; // Focus the first column of the last chip in the array - chips.last.focus(); - expectLastCellFocused(); + lastChip.focus(); + expect(manager.activeRowIndex).toEqual(lastRowIndex); + expect(manager.activeColumnIndex).toEqual(0); // Press the LEFT arrow dispatchKeyboardEvent(lastNativeChip, 'keydown', LEFT_ARROW); @@ -287,8 +292,11 @@ describe('MDC-based MatChipGrid', () => { let nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row'); let firstNativeChip = nativeChips[0] as HTMLElement; + let array = chips.toArray(); + let firstItem = array[0]; + // Focus the first column of the first chip in the array - chips.first.focus(); + firstItem.focus(); expect(manager.activeRowIndex).toEqual(0); expect(manager.activeColumnIndex).toEqual(0); @@ -314,21 +322,24 @@ describe('MDC-based MatChipGrid', () => { }); describe('RTL', () => { - let fixture: ComponentFixture; - beforeEach(() => { - fixture = setupStandardGrid('rtl'); + setupStandardGrid('rtl'); + manager = chipGridInstance._keyManager; }); it('should focus previous column when press RIGHT ARROW', () => { let nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row'); let lastNativeChip = nativeChips[nativeChips.length - 1] as HTMLElement; - const lastRowIndex = chips.length - 1; + let array = chips.toArray(); + let lastRowIndex = array.length - 1; + let lastItem = array[lastRowIndex]; // Focus the first column of the last chip in the array - chips.last.focus(); - expectLastCellFocused(); + lastItem.focus(); + expect(manager.activeRowIndex).toEqual(lastRowIndex); + expect(manager.activeColumnIndex).toEqual(0); + // Press the RIGHT arrow dispatchKeyboardEvent(lastNativeChip, 'keydown', RIGHT_ARROW); @@ -344,11 +355,15 @@ describe('MDC-based MatChipGrid', () => { let nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row'); let firstNativeChip = nativeChips[0] as HTMLElement; + let array = chips.toArray(); + let firstItem = array[0]; + // Focus the first column of the first chip in the array - chips.first.focus(); + firstItem.focus(); expect(manager.activeRowIndex).toEqual(0); expect(manager.activeColumnIndex).toEqual(0); + // Press the LEFT arrow dispatchKeyboardEvent(firstNativeChip, 'keydown', LEFT_ARROW); chipGridInstance._blur(); // Simulate focus leaving the list and going to the chip. @@ -394,110 +409,123 @@ describe('MDC-based MatChipGrid', () => { })); }); - describe('keydown behavior', () => { - let fixture: ComponentFixture; + it('should account for the direction changing', () => { + setupStandardGrid(); + manager = chipGridInstance._keyManager; - beforeEach(() => { - fixture = setupStandardGrid(); - }); + let nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row'); + let firstNativeChip = nativeChips[0] as HTMLElement; - it('should account for the direction changing', () => { - const firstNativeChip = - chipGridNativeElement.querySelectorAll('mat-chip-row')[0] as HTMLElement; + let RIGHT_EVENT = createKeyboardEvent('keydown', RIGHT_ARROW); + let array = chips.toArray(); + let firstItem = array[0]; - const RIGHT_EVENT = createKeyboardEvent('keydown', RIGHT_ARROW); + firstItem.focus(); + expect(manager.activeRowIndex).toBe(0); + expect(manager.activeColumnIndex).toBe(0); - chips.first.focus(); - expect(manager.activeRowIndex).toBe(0); - expect(manager.activeColumnIndex).toBe(0); + dispatchEvent(firstNativeChip, RIGHT_EVENT); + chipGridInstance._blur(); + fixture.detectChanges(); - dispatchEvent(firstNativeChip, RIGHT_EVENT); - chipGridInstance._blur(); - fixture.detectChanges(); + expect(manager.activeRowIndex).toBe(1); + expect(manager.activeColumnIndex).toBe(0); - expect(manager.activeRowIndex).toBe(1); - expect(manager.activeColumnIndex).toBe(0); + dirChange.next('rtl'); + fixture.detectChanges(); - dirChange.next('rtl'); - fixture.detectChanges(); + chipGridInstance._keydown(RIGHT_EVENT); + chipGridInstance._blur(); + fixture.detectChanges(); - chipGridInstance._keydown(RIGHT_EVENT); - chipGridInstance._blur(); - fixture.detectChanges(); + expect(manager.activeRowIndex).toBe(0); + expect(manager.activeColumnIndex).toBe(0); + }); - expect(manager.activeRowIndex).toBe(0); - expect(manager.activeColumnIndex).toBe(0); - }); + it('should move focus to the first chip when pressing HOME', () => { + setupStandardGrid(); + manager = chipGridInstance._keyManager; - it('should move focus to the first chip when pressing HOME', () => { - const nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row'); - const lastNativeChip = nativeChips[nativeChips.length - 1] as HTMLElement; + const nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row'); + const lastNativeChip = nativeChips[nativeChips.length - 1] as HTMLElement; - const HOME_EVENT = createKeyboardEvent('keydown', HOME); - chips.last.focus(); + const HOME_EVENT = createKeyboardEvent('keydown', HOME); + const array = chips.toArray(); + const lastItem = array[array.length - 1]; - expect(manager.activeRowIndex).toBe(4); - expect(manager.activeColumnIndex).toBe(0); + lastItem.focus(); + expect(manager.activeRowIndex).toBe(4); + expect(manager.activeColumnIndex).toBe(0); - dispatchEvent(lastNativeChip, HOME_EVENT); - fixture.detectChanges(); + dispatchEvent(lastNativeChip, HOME_EVENT); + fixture.detectChanges(); - expect(HOME_EVENT.defaultPrevented).toBe(true); - expect(manager.activeRowIndex).toBe(0); - expect(manager.activeColumnIndex).toBe(0); - }); + expect(HOME_EVENT.defaultPrevented).toBe(true); + expect(manager.activeRowIndex).toBe(0); + expect(manager.activeColumnIndex).toBe(0); + }); - it('should move focus to the last chip when pressing END', () => { - const nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row'); - const firstNativeChip = nativeChips[0] as HTMLElement; + it('should move focus to the last chip when pressing END', () => { + setupStandardGrid(); + manager = chipGridInstance._keyManager; - const END_EVENT = createKeyboardEvent('keydown', END); - chips.first.focus(); + const nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row'); + const firstNativeChip = nativeChips[0] as HTMLElement; - expect(manager.activeRowIndex).toBe(0); - expect(manager.activeColumnIndex).toBe(0); + const END_EVENT = createKeyboardEvent('keydown', END); + const array = chips.toArray(); + const firstItem = array[0]; - dispatchEvent(firstNativeChip, END_EVENT); - fixture.detectChanges(); + firstItem.focus(); + expect(manager.activeRowIndex).toBe(0); + expect(manager.activeColumnIndex).toBe(0); - expect(END_EVENT.defaultPrevented).toBe(true); - expect(manager.activeRowIndex).toBe(4); - expect(manager.activeColumnIndex).toBe(0); - }); + dispatchEvent(firstNativeChip, END_EVENT); + fixture.detectChanges(); - it('should ignore all non-tab navigation keyboard events from an editing chip', () => { - testComponent.editable = true; - fixture.detectChanges(); + expect(END_EVENT.defaultPrevented).toBe(true); + expect(manager.activeRowIndex).toBe(4); + expect(manager.activeColumnIndex).toBe(0); + }); - chips.first.focus(); + it('should ignore all non-tab navigation keyboard events from an editing chip', () => { + setupStandardGrid(); + manager = chipGridInstance._keyManager; + testComponent.editable = true; + fixture.detectChanges(); - dispatchKeyboardEvent(document.activeElement!, 'keydown', ENTER, 'Enter'); - fixture.detectChanges(); + const array = chips.toArray(); + const firstItem = array[0]; - const activeRowIndex = manager.activeRowIndex; - const activeColumnIndex = manager.activeColumnIndex; + firstItem.focus(); + dispatchKeyboardEvent(document.activeElement!, 'keydown', ENTER, 'Enter'); + fixture.detectChanges(); - const KEYS_TO_IGNORE = [HOME, END, LEFT_ARROW, RIGHT_ARROW]; - for (const key of KEYS_TO_IGNORE) { - dispatchKeyboardEvent(document.activeElement!, 'keydown', key); - fixture.detectChanges(); + const activeRowIndex = manager.activeRowIndex; + const activeColumnIndex = manager.activeColumnIndex; - expect(manager.activeRowIndex).toBe(activeRowIndex); - expect(manager.activeColumnIndex).toBe(activeColumnIndex); - } - }); + const KEYS_TO_IGNORE = [HOME, END, LEFT_ARROW, RIGHT_ARROW]; + for (const key of KEYS_TO_IGNORE) { + dispatchKeyboardEvent(document.activeElement!, 'keydown', key); + fixture.detectChanges(); + + expect(manager.activeRowIndex).toBe(activeRowIndex); + expect(manager.activeColumnIndex).toBe(activeColumnIndex); + } }); }); }); describe('FormFieldChipGrid', () => { - let fixture: ComponentFixture; - beforeEach(() => { - fixture = setupInputGrid(); + setupInputGrid(); }); describe('keyboard behavior', () => { + beforeEach(() => { + manager = chipGridInstance._keyManager; + }); + it('should maintain focus if the active chip is deleted', () => { const secondChip = fixture.nativeElement.querySelectorAll('.mat-mdc-chip')[1]; @@ -513,19 +541,22 @@ describe('MDC-based MatChipGrid', () => { }); describe('when the input has focus', () => { + it('should not focus the last chip when press DELETE', () => { let nativeInput = fixture.nativeElement.querySelector('input'); // Focus the input nativeInput.focus(); - expectNoCellFocused(); + expect(manager.activeRowIndex).toBe(-1); + expect(manager.activeColumnIndex).toBe(-1); // Press the DELETE key dispatchKeyboardEvent(nativeInput, 'keydown', DELETE); fixture.detectChanges(); // It doesn't focus the last chip - expectNoCellFocused(); + expect(manager.activeRowIndex).toEqual(-1); + expect(manager.activeColumnIndex).toBe(-1); }); it('should focus the last chip when press BACKSPACE', () => { @@ -533,14 +564,16 @@ describe('MDC-based MatChipGrid', () => { // Focus the input nativeInput.focus(); - expectNoCellFocused(); + expect(manager.activeRowIndex).toBe(-1); + expect(manager.activeColumnIndex).toBe(-1); // Press the BACKSPACE key dispatchKeyboardEvent(nativeInput, 'keydown', BACKSPACE); fixture.detectChanges(); // It focuses the last chip - expectLastCellFocused(); + expect(manager.activeRowIndex).toEqual(chips.length - 1); + expect(manager.activeColumnIndex).toBe(0); }); }); }); @@ -567,45 +600,40 @@ describe('MDC-based MatChipGrid', () => { }); describe('with chip remove', () => { - let fixture: ComponentFixture; let chipGrid: MatChipGrid; let chipRemoveDebugElements: DebugElement[]; beforeEach(() => { fixture = createComponent(ChipGridWithRemove); + fixture.detectChanges(); chipGrid = fixture.debugElement.query(By.directive(MatChipGrid))!.componentInstance; chipRemoveDebugElements = fixture.debugElement.queryAll(By.directive(MatChipRemove)); + chips = chipGrid._chips; }); it('should properly focus next item if chip is removed through click', () => { - chips.get(2)!.focus(); + chips.toArray()[2].focus(); // Destroy the third focused chip by dispatching a bubbling click event on the // associated chip remove element. dispatchMouseEvent(chipRemoveDebugElements[2].nativeElement, 'click'); fixture.detectChanges(); - expect(chips.get(2)!.value).not.toBe(2, 'Expected the third chip to be removed.'); + expect(chips.toArray()[2].value).not.toBe(2, 'Expected the third chip to be removed.'); expect(chipGrid._keyManager.activeRowIndex).toBe(2); }); }); describe('chip grid with chip input', () => { - let fixture: ComponentFixture; let nativeChips: HTMLElement[]; - let nativeInput: HTMLInputElement; - let nativeChipGrid: HTMLElement; beforeEach(() => { fixture = createComponent(InputChipGrid); + fixture.detectChanges(); nativeChips = fixture.debugElement.queryAll(By.css('mat-chip-row')) .map((chip) => chip.nativeElement); - - nativeChipGrid = fixture.debugElement.query(By.css('mat-chip-grid'))!.nativeElement; - - nativeInput = fixture.nativeElement.querySelector('input'); }); it('should take an initial view value with reactive forms', () => { @@ -630,6 +658,7 @@ describe('MDC-based MatChipGrid', () => { expect(fixture.componentInstance.control.value) .toEqual(null, `Expected the control's value to be empty initially.`); + const nativeInput = fixture.nativeElement.querySelector('input'); nativeInput.focus(); typeInElement(nativeInput, '123'); @@ -658,6 +687,7 @@ describe('MDC-based MatChipGrid', () => { expect(fixture.componentInstance.control.touched) .toBe(false, 'Expected the control to start off as untouched.'); + const nativeChipGrid = fixture.debugElement.query(By.css('mat-chip-grid'))!.nativeElement; dispatchFakeEvent(nativeChipGrid, 'blur'); tick(); @@ -670,6 +700,7 @@ describe('MDC-based MatChipGrid', () => { .toBe(false, 'Expected the control to start off as untouched.'); fixture.componentInstance.control.disable(); + const nativeChipGrid = fixture.debugElement.query(By.css('mat-chip-grid'))!.nativeElement; dispatchFakeEvent(nativeChipGrid, 'blur'); tick(); @@ -682,6 +713,7 @@ describe('MDC-based MatChipGrid', () => { expect(fixture.componentInstance.control.dirty) .toEqual(false, `Expected control to start out pristine.`); + const nativeInput = fixture.nativeElement.querySelector('input'); nativeInput.focus(); typeInElement(nativeInput, '123'); @@ -738,6 +770,7 @@ describe('MDC-based MatChipGrid', () => { })); it('should keep focus on the input after adding the first chip', fakeAsync(() => { + const nativeInput = fixture.nativeElement.querySelector('input'); const chipEls = Array.from( fixture.nativeElement.querySelectorAll('mat-chip-row')).reverse(); @@ -782,64 +815,16 @@ describe('MDC-based MatChipGrid', () => { fixture.detectChanges(); expect(input.getAttribute('aria-invalid')).toBe('false'); })); - - describe('when the input has focus', () => { - beforeEach(() => { - nativeInput.focus(); - expectNoCellFocused(); - }); - - it('should not focus the last chip when pressing DELETE', () => { - dispatchKeyboardEvent(nativeInput, 'keydown', DELETE); - expectNoCellFocused(); - }); - - it('should focus the last chip when pressing BACKSPACE when input is empty', () => { - dispatchKeyboardEvent(nativeInput, 'keydown', BACKSPACE); - expectLastCellFocused(); - }); - - it('should not focus the last chip when pressing BACKSPACE after changing input, ' + - 'until BACKSPACE is released and pressed again', () => { - // Change the input - dispatchKeyboardEvent(nativeInput, 'keydown', A); - - // It shouldn't focus until backspace is released and pressed again - dispatchKeyboardEvent(nativeInput, 'keydown', BACKSPACE); - dispatchKeyboardEvent(nativeInput, 'keydown', BACKSPACE); - dispatchKeyboardEvent(nativeInput, 'keydown', BACKSPACE); - expectNoCellFocused(); - - // Still not focused - dispatchKeyboardEvent(nativeInput, 'keyup', BACKSPACE); - expectNoCellFocused(); - - // Only now should it focus the last element - dispatchKeyboardEvent(nativeInput, 'keydown', BACKSPACE); - expectLastCellFocused(); - }); - - it('should focus last chip after pressing BACKSPACE after creating a chip', () => { - // Create a chip - typeInElement(nativeInput, '123'); - dispatchKeyboardEvent(nativeInput, 'keydown', ENTER); - - expectNoCellFocused(); - - dispatchKeyboardEvent(nativeInput, 'keydown', BACKSPACE); - expectLastCellFocused(); - }); - }); }); describe('error messages', () => { - let fixture: ComponentFixture; let errorTestComponent: ChipGridWithFormErrorMessages; let containerEl: HTMLElement; let chipGridEl: HTMLElement; beforeEach(() => { fixture = createComponent(ChipGridWithFormErrorMessages); + fixture.detectChanges(); errorTestComponent = fixture.componentInstance; containerEl = fixture.debugElement.query(By.css('mat-form-field'))!.nativeElement; chipGridEl = fixture.debugElement.query(By.css('mat-chip-grid'))!.nativeElement; @@ -947,12 +932,9 @@ describe('MDC-based MatChipGrid', () => { }); }); - function createComponent( - component: Type, - providers: Provider[] = [], - animationsModule: - Type | Type = NoopAnimationsModule - ): ComponentFixture { + function createComponent(component: Type, providers: Provider[] = [], animationsModule: + Type | Type = NoopAnimationsModule): + ComponentFixture { TestBed.configureTestingModule({ imports: [ FormsModule, @@ -964,15 +946,22 @@ describe('MDC-based MatChipGrid', () => { ], declarations: [component], providers: [ - { - provide: NgZone, - useFactory: () => zone = new MockNgZone() - }, + {provide: NgZone, useFactory: () => zone = new MockNgZone()}, ...providers ] }).compileComponents(); - const fixture = TestBed.createComponent(component); + return TestBed.createComponent(component); + } + + function setupStandardGrid(direction: Direction = 'ltr') { + dirChange = new Subject(); + fixture = createComponent(StandardChipGrid, [{ + provide: Directionality, useFactory: () => ({ + value: direction.toLowerCase(), + change: dirChange + }) + }]); fixture.detectChanges(); chipGridDebugElement = fixture.debugElement.query(By.directive(MatChipGrid))!; @@ -980,27 +969,17 @@ describe('MDC-based MatChipGrid', () => { chipGridInstance = chipGridDebugElement.componentInstance; testComponent = fixture.debugElement.componentInstance; chips = chipGridInstance._chips; - manager = chipGridInstance._keyManager; - - return fixture; - } - - function setupStandardGrid(direction: Direction = 'ltr') { - dirChange = new Subject(); - - return createComponent(StandardChipGrid, [ - { - provide: Directionality, - useFactory: () => ({ - value: direction.toLowerCase(), - change: dirChange - }) - } - ]); } function setupInputGrid() { - return createComponent(FormFieldChipGrid); + fixture = createComponent(FormFieldChipGrid); + fixture.detectChanges(); + + chipGridDebugElement = fixture.debugElement.query(By.directive(MatChipGrid))!; + chipGridNativeElement = chipGridDebugElement.nativeElement; + chipGridInstance = chipGridDebugElement.componentInstance; + testComponent = fixture.debugElement.componentInstance; + chips = chipGridInstance._chips; } }); @@ -1080,18 +1059,21 @@ class InputChipGrid { isRequired: boolean; add(event: MatChipInputEvent): void { - const value = (event.value || '').trim(); + let input = event.input; + let value = event.value; // Add our foods - if (value) { + if ((value || '').trim()) { this.foods.push({ - value: `${value.toLowerCase()}-${this.foods.length}`, - viewValue: value, + value: `${value.trim().toLowerCase()}-${this.foods.length}`, + viewValue: value.trim() }); } // Reset the input value - event.chipInput!.clear(); + if (input) { + input.value = ''; + } } remove(food: any): void { diff --git a/src/material-experimental/mdc-chips/chip-grid.ts b/src/material-experimental/mdc-chips/chip-grid.ts index 6046fef793c7..dac73e8aee22 100644 --- a/src/material-experimental/mdc-chips/chip-grid.ts +++ b/src/material-experimental/mdc-chips/chip-grid.ts @@ -8,7 +8,7 @@ import {Directionality} from '@angular/cdk/bidi'; import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; -import {TAB} from '@angular/cdk/keycodes'; +import {BACKSPACE, TAB} from '@angular/cdk/keycodes'; import { AfterContentInit, AfterViewInit, @@ -43,6 +43,7 @@ import {MatChipRow} from './chip-row'; import {MatChipSet} from './chip-set'; import {GridFocusKeyManager} from './grid-focus-key-manager'; + /** Change event object that is emitted when the chip grid value has changed. */ export class MatChipGridChange { constructor( @@ -108,12 +109,12 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn */ readonly controlType: string = 'mat-chip-grid'; - /** Subscription to focus changes in the chips. */ - private _chipFocusSubscription: Subscription | null; - /** Subscription to blur changes in the chips. */ private _chipBlurSubscription: Subscription | null; + /** Subscription to focus changes in the chips. */ + private _chipFocusSubscription: Subscription | null; + /** The chip input to add more chips */ protected _chipInput: MatChipTextControl; @@ -407,15 +408,19 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn const target = event.target as HTMLElement; const keyCode = event.keyCode; const manager = this._keyManager; - if (keyCode === TAB && target.id !== this._chipInput!.id) { this._allowFocusEscape(); } else if (this._originatesFromEditingChip(event)) { // No-op, let the editing chip handle all keyboard events except for Tab. + } else if (keyCode === BACKSPACE && this._isEmptyInput(target)) { + // If they are on an empty input and hit backspace, focus the last chip + if (this._chips.length) { + manager.setLastCellActive(); + } + event.preventDefault(); } else if (this._originatesFromChip(event)) { manager.onKeydown(event); } - this.stateChanges.next(); } @@ -453,7 +458,7 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn } } - /** Subscribes to chip focus events. */ + /** Subscribes to chip focus events. */ private _listenToChipsFocus(): void { this._chipFocusSubscription = this.chipFocusChanges.subscribe((event: MatChipEvent) => { let chipIndex: number = this._chips.toArray().indexOf(event.chip as MatChipRow); @@ -472,7 +477,7 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn }); } - /** Emits change event to set the model value. */ + /** Emits change event to set the model value. */ private _propagateChanges(): void { const valueToEmit = this._chips.length ? this._chips.toArray().map( chip => chip.value) : []; @@ -515,6 +520,15 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn this._chipInput.focus(); } + /** Returns true if element is an input with no value. */ + private _isEmptyInput(element: HTMLElement): boolean { + if (element && element.id === this._chipInput!.id) { + return this._chipInput.empty; + } + + return false; + } + static ngAcceptInputType_disabled: BooleanInput; static ngAcceptInputType_required: BooleanInput; } diff --git a/src/material-experimental/mdc-chips/chip-input.ts b/src/material-experimental/mdc-chips/chip-input.ts index f0d7e00c7740..aa3faafd5bdf 100644 --- a/src/material-experimental/mdc-chips/chip-input.ts +++ b/src/material-experimental/mdc-chips/chip-input.ts @@ -7,39 +7,20 @@ */ import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; -import {BACKSPACE, hasModifierKey, TAB} from '@angular/cdk/keycodes'; -import { - AfterContentInit, - Directive, - ElementRef, - EventEmitter, - Inject, - Input, - OnChanges, - OnDestroy, - Output -} from '@angular/core'; +import {hasModifierKey, TAB} from '@angular/cdk/keycodes'; +import {Directive, ElementRef, EventEmitter, Inject, Input, OnChanges, Output} from '@angular/core'; import {MatChipsDefaultOptions, MAT_CHIPS_DEFAULT_OPTIONS} from './chip-default-options'; import {MatChipGrid} from './chip-grid'; import {MatChipTextControl} from './chip-text-control'; + /** Represents an input event on a `matChipInput`. */ export interface MatChipInputEvent { - /** - * The native `` element that the event is being fired for. - * @deprecated Use `MatChipInputEvent#chipInput.inputElement` instead. - * @breaking-change 13.0.0 This property will be removed. - */ + /** The native `` element that the event is being fired for. */ input: HTMLInputElement; /** The value of the input. */ value: string; - - /** - * Reference to the chip input that emitted the event. - * @breaking-change 13.0.0 This property will be made required. - */ - chipInput?: MatChipInput; } // Increasing integer for generating unique ids. @@ -58,7 +39,6 @@ let nextUniqueId = 0; // the MDC chips were landed initially with it. 'class': 'mat-mdc-chip-input mat-mdc-input-element mdc-text-field__input mat-input-element', '(keydown)': '_keydown($event)', - '(keyup)': '_keyup($event)', '(blur)': '_blur()', '(focus)': '_focus()', '(input)': '_onInput()', @@ -69,10 +49,7 @@ let nextUniqueId = 0; '[attr.aria-required]': '_chipGrid && _chipGrid.required || null', } }) -export class MatChipInput implements MatChipTextControl, AfterContentInit, OnChanges, OnDestroy { - /** Used to prevent focus moving to chips while user is holding backspace */ - private _focusLastChipOnBackspace: boolean; - +export class MatChipInput implements MatChipTextControl, OnChanges { /** Whether the control is focused. */ focused: boolean = false; _chipGrid: MatChipGrid; @@ -120,64 +97,32 @@ export class MatChipInput implements MatChipTextControl, AfterContentInit, OnCha private _disabled: boolean = false; /** Whether the input is empty. */ - get empty(): boolean { return !this.inputElement.value; } + get empty(): boolean { return !this._inputElement.value; } /** The native input element to which this directive is attached. */ - readonly inputElement: HTMLInputElement; + protected _inputElement: HTMLInputElement; constructor( protected _elementRef: ElementRef, @Inject(MAT_CHIPS_DEFAULT_OPTIONS) private _defaultOptions: MatChipsDefaultOptions) { - this.inputElement = this._elementRef.nativeElement as HTMLInputElement; + this._inputElement = this._elementRef.nativeElement as HTMLInputElement; } ngOnChanges() { this._chipGrid.stateChanges.next(); } - ngOnDestroy(): void { - this.chipEnd.complete(); - } - - ngAfterContentInit(): void { - this._focusLastChipOnBackspace = this.empty; - } - /** Utility method to make host definition/tests more clear. */ _keydown(event?: KeyboardEvent) { - if (event) { - // Allow the user's focus to escape when they're tabbing forward. Note that we don't - // want to do this when going backwards, because focus should go back to the first chip. - if (event.keyCode === TAB && !hasModifierKey(event, 'shiftKey')) { - this._chipGrid._allowFocusEscape(); - } - - // To prevent the user from accidentally deleting chips when pressing BACKSPACE continuously, - // We focus the last chip on backspace only after the user has released the backspace button, - // And the input is empty (see behaviour in _keyup) - if (event.keyCode === BACKSPACE && this._focusLastChipOnBackspace) { - this._chipGrid._keyManager.setLastCellActive(); - event.preventDefault(); - return; - } else { - this._focusLastChipOnBackspace = false; - } + // Allow the user's focus to escape when they're tabbing forward. Note that we don't + // want to do this when going backwards, because focus should go back to the first chip. + if (event && event.keyCode === TAB && !hasModifierKey(event, 'shiftKey')) { + this._chipGrid._allowFocusEscape(); } this._emitChipEnd(event); } - /** - * Pass events to the keyboard manager. Available here for tests. - */ - _keyup(event: KeyboardEvent) { - // Allow user to move focus to chips next time he presses backspace - if (!this._focusLastChipOnBackspace && event.keyCode === BACKSPACE && this.empty) { - this._focusLastChipOnBackspace = true; - event.preventDefault(); - } - } - /** Checks to see if the blur should emit the (chipEnd) event. */ _blur() { if (this.addOnBlur) { @@ -198,18 +143,15 @@ export class MatChipInput implements MatChipTextControl, AfterContentInit, OnCha /** Checks to see if the (chipEnd) event needs to be emitted. */ _emitChipEnd(event?: KeyboardEvent) { - if (!this.inputElement.value && !!event) { + if (!this._inputElement.value && !!event) { this._chipGrid._keydown(event); } - if (!event || this._isSeparatorKey(event)) { - this.chipEnd.emit({ - input: this.inputElement, - value: this.inputElement.value, - chipInput: this, - }); + this.chipEnd.emit({ input: this._inputElement, value: this._inputElement.value }); - event?.preventDefault(); + if (event) { + event.preventDefault(); + } } } @@ -220,13 +162,7 @@ export class MatChipInput implements MatChipTextControl, AfterContentInit, OnCha /** Focuses the input. */ focus(): void { - this.inputElement.focus(); - } - - /** Clears the input */ - clear(): void { - this.inputElement.value = ''; - this._focusLastChipOnBackspace = true; + this._inputElement.focus(); } /** Checks whether a keycode is one of the configured separators. */ diff --git a/src/material/chips/chip-input.ts b/src/material/chips/chip-input.ts index 61b42772e313..ae5ae364d5ee 100644 --- a/src/material/chips/chip-input.ts +++ b/src/material/chips/chip-input.ts @@ -7,39 +7,20 @@ */ import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; -import {BACKSPACE, hasModifierKey, TAB} from '@angular/cdk/keycodes'; -import { - AfterContentInit, - Directive, - ElementRef, - EventEmitter, - Inject, - Input, - OnChanges, - OnDestroy, - Output -} from '@angular/core'; +import {hasModifierKey, TAB} from '@angular/cdk/keycodes'; +import {Directive, ElementRef, EventEmitter, Inject, Input, OnChanges, Output} from '@angular/core'; import {MatChipsDefaultOptions, MAT_CHIPS_DEFAULT_OPTIONS} from './chip-default-options'; import {MatChipList} from './chip-list'; import {MatChipTextControl} from './chip-text-control'; + /** Represents an input event on a `matChipInput`. */ export interface MatChipInputEvent { - /** - * The native `` element that the event is being fired for. - * @deprecated Use `MatChipInputEvent#chipInput.inputElement` instead. - * @breaking-change 13.0.0 This property will be removed. - */ + /** The native `` element that the event is being fired for. */ input: HTMLInputElement; /** The value of the input. */ value: string; - - /** - * Reference to the chip input that emitted the event. - * @breaking-change 13.0.0 This property will be made required. - */ - chipInput?: MatChipInput; } // Increasing integer for generating unique ids. @@ -55,7 +36,6 @@ let nextUniqueId = 0; host: { 'class': 'mat-chip-input mat-input-element', '(keydown)': '_keydown($event)', - '(keyup)': '_keyup($event)', '(blur)': '_blur()', '(focus)': '_focus()', '(input)': '_onInput()', @@ -66,10 +46,7 @@ let nextUniqueId = 0; '[attr.aria-required]': '_chipList && _chipList.required || null', } }) -export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy, AfterContentInit { - /** Used to prevent focus moving to chips while user is holding backspace */ - private _focusLastChipOnBackspace: boolean; - +export class MatChipInput implements MatChipTextControl, OnChanges { /** Whether the control is focused. */ focused: boolean = false; _chipList: MatChipList; @@ -117,64 +94,32 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy, A private _disabled: boolean = false; /** Whether the input is empty. */ - get empty(): boolean { return !this.inputElement.value; } + get empty(): boolean { return !this._inputElement.value; } /** The native input element to which this directive is attached. */ - readonly inputElement: HTMLInputElement; + protected _inputElement: HTMLInputElement; constructor( protected _elementRef: ElementRef, @Inject(MAT_CHIPS_DEFAULT_OPTIONS) private _defaultOptions: MatChipsDefaultOptions) { - this.inputElement = this._elementRef.nativeElement as HTMLInputElement; + this._inputElement = this._elementRef.nativeElement as HTMLInputElement; } - ngOnChanges(): void { + ngOnChanges() { this._chipList.stateChanges.next(); } - ngOnDestroy(): void { - this.chipEnd.complete(); - } - - ngAfterContentInit(): void { - this._focusLastChipOnBackspace = this.empty; - } - /** Utility method to make host definition/tests more clear. */ _keydown(event?: KeyboardEvent) { - if (event) { - // Allow the user's focus to escape when they're tabbing forward. Note that we don't - // want to do this when going backwards, because focus should go back to the first chip. - if (event.keyCode === TAB && !hasModifierKey(event, 'shiftKey')) { - this._chipList._allowFocusEscape(); - } - - // To prevent the user from accidentally deleting chips when pressing BACKSPACE continuously, - // We focus the last chip on backspace only after the user has released the backspace button, - // and the input is empty (see behaviour in _keyup) - if (event.keyCode === BACKSPACE && this._focusLastChipOnBackspace) { - this._chipList._keyManager.setLastItemActive(); - event.preventDefault(); - return; - } else { - this._focusLastChipOnBackspace = false; - } + // Allow the user's focus to escape when they're tabbing forward. Note that we don't + // want to do this when going backwards, because focus should go back to the first chip. + if (event && event.keyCode === TAB && !hasModifierKey(event, 'shiftKey')) { + this._chipList._allowFocusEscape(); } this._emitChipEnd(event); } - /** - * Pass events to the keyboard manager. Available here for tests. - */ - _keyup(event: KeyboardEvent) { - // Allow user to move focus to chips next time he presses backspace - if (!this._focusLastChipOnBackspace && event.keyCode === BACKSPACE && this.empty) { - this._focusLastChipOnBackspace = true; - event.preventDefault(); - } - } - /** Checks to see if the blur should emit the (chipEnd) event. */ _blur() { if (this.addOnBlur) { @@ -195,18 +140,15 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy, A /** Checks to see if the (chipEnd) event needs to be emitted. */ _emitChipEnd(event?: KeyboardEvent) { - if (!this.inputElement.value && !!event) { + if (!this._inputElement.value && !!event) { this._chipList._keydown(event); } - if (!event || this._isSeparatorKey(event)) { - this.chipEnd.emit({ - input: this.inputElement, - value: this.inputElement.value, - chipInput: this, - }); + this.chipEnd.emit({ input: this._inputElement, value: this._inputElement.value }); - event?.preventDefault(); + if (event) { + event.preventDefault(); + } } } @@ -217,13 +159,7 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy, A /** Focuses the input. */ focus(options?: FocusOptions): void { - this.inputElement.focus(options); - } - - /** Clears the input */ - clear(): void { - this.inputElement.value = ''; - this._focusLastChipOnBackspace = true; + this._inputElement.focus(options); } /** Checks whether a keycode is one of the configured separators. */ diff --git a/src/material/chips/chip-list.spec.ts b/src/material/chips/chip-list.spec.ts index 6b0b95df2cbc..47b6eb53cf1c 100644 --- a/src/material/chips/chip-list.spec.ts +++ b/src/material/chips/chip-list.spec.ts @@ -2,7 +2,6 @@ import {animate, style, transition, trigger} from '@angular/animations'; import {FocusKeyManager} from '@angular/cdk/a11y'; import {Direction, Directionality} from '@angular/cdk/bidi'; import { - A, BACKSPACE, DELETE, END, @@ -1037,6 +1036,7 @@ describe('MatChipList', () => { .toBeFalsy(`Expected chip with the old value not to be selected.`); }); + it('should clear the selection when the control is reset', () => { const array = fixture.componentInstance.chips.toArray(); @@ -1096,6 +1096,7 @@ describe('MatChipList', () => { .toEqual(false, `Expected control to stay pristine after programmatic change.`); }); + it('should set an asterisk after the placeholder if the control is required', () => { let requiredMarker = fixture.debugElement.query(By.css('.mat-form-field-required-marker'))!; expect(requiredMarker) @@ -1150,63 +1151,45 @@ describe('MatChipList', () => { }); describe('keyboard behavior', () => { - let nativeInput: HTMLInputElement; - - const expectNoItemFocused = () => expect(manager.activeItemIndex).toBe(-1); - const expectLastItemFocused = () => expect(manager.activeItemIndex).toEqual(chips.length - 1); - beforeEach(() => { chipListDebugElement = fixture.debugElement.query(By.directive(MatChipList))!; chipListInstance = chipListDebugElement.componentInstance; chips = chipListInstance.chips; manager = fixture.componentInstance.chipList._keyManager; - nativeInput = fixture.nativeElement.querySelector('input'); - nativeInput.focus(); - expectNoItemFocused(); }); describe('when the input has focus', () => { - it('should not focus the last chip when pressing DELETE', () => { - dispatchKeyboardEvent(nativeInput, 'keydown', DELETE); - expectNoItemFocused(); - }); - - it('should focus the last chip when pressing BACKSPACE when input is empty', () => { - dispatchKeyboardEvent(nativeInput, 'keydown', BACKSPACE); - expectLastItemFocused(); - }); - - it('should not focus the last chip when pressing BACKSPACE after changing input, ' + - 'until BACKSPACE is released and pressed again', () => { - // Change the input - dispatchKeyboardEvent(nativeInput, 'keydown', A); + it('should not focus the last chip when press DELETE', () => { + let nativeInput = fixture.nativeElement.querySelector('input'); - // It shouldn't focus until backspace is released and pressed again - dispatchKeyboardEvent(nativeInput, 'keydown', BACKSPACE); - dispatchKeyboardEvent(nativeInput, 'keydown', BACKSPACE); - dispatchKeyboardEvent(nativeInput, 'keydown', BACKSPACE); - expectNoItemFocused(); + // Focus the input + nativeInput.focus(); + expect(manager.activeItemIndex).toBe(-1); - // Still not focused - dispatchKeyboardEvent(nativeInput, 'keyup', BACKSPACE); - expectNoItemFocused(); + // Press the DELETE key + dispatchKeyboardEvent(nativeInput, 'keydown', DELETE); + fixture.detectChanges(); - // Only now should it focus the last element - dispatchKeyboardEvent(nativeInput, 'keydown', BACKSPACE); - expectLastItemFocused(); + // It doesn't focus the last chip + expect(manager.activeItemIndex).toEqual(-1); }); - it('should focus last chip after pressing BACKSPACE after creating a chip', () => { - // Create a chip - typeInElement(nativeInput, '123'); - dispatchKeyboardEvent(nativeInput, 'keydown', ENTER); + it('should focus the last chip when press BACKSPACE', () => { + let nativeInput = fixture.nativeElement.querySelector('input'); - expectNoItemFocused(); + // Focus the input + nativeInput.focus(); + expect(manager.activeItemIndex).toBe(-1); + // Press the BACKSPACE key dispatchKeyboardEvent(nativeInput, 'keydown', BACKSPACE); - expectLastItemFocused(); + fixture.detectChanges(); + + // It focuses the last chip + expect(manager.activeItemIndex).toEqual(chips.length - 1); }); + }); }); }); @@ -1507,13 +1490,11 @@ class MultiSelectionChipList { {{ food.viewValue }} - + (matChipInputTokenEnd)="add($event)" />/> ` }) @@ -1535,18 +1516,21 @@ class InputChipList { isRequired: boolean; add(event: MatChipInputEvent): void { - const value = (event.value || '').trim(); + let input = event.input; + let value = event.value; // Add our foods - if (value) { + if ((value || '').trim()) { this.foods.push({ - value: `${value.toLowerCase()}-${this.foods.length}`, - viewValue: value + value: `${value.trim().toLowerCase()}-${this.foods.length}`, + viewValue: value.trim() }); } - // Clear the input value - event.chipInput!.clear(); + // Reset the input value + if (input) { + input.value = ''; + } } remove(food: any): void { diff --git a/src/material/chips/chip-list.ts b/src/material/chips/chip-list.ts index ed88cf512121..d2caf2f0afd5 100644 --- a/src/material/chips/chip-list.ts +++ b/src/material/chips/chip-list.ts @@ -10,6 +10,7 @@ import {FocusKeyManager} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; import {SelectionModel} from '@angular/cdk/collections'; +import {BACKSPACE} from '@angular/cdk/keycodes'; import { AfterContentInit, ChangeDetectionStrategy, @@ -41,6 +42,7 @@ import {startWith, takeUntil} from 'rxjs/operators'; import {MatChip, MatChipEvent, MatChipSelectionChange} from './chip'; import {MatChipTextControl} from './chip-text-control'; + // Boilerplate for applying mixins to MatChipList. /** @docs-private */ class MatChipListBase { @@ -66,6 +68,7 @@ export class MatChipListChange { public value: any) { } } + /** * A material design chips component (named ChipList for its similarity to the List component). */ @@ -412,6 +415,7 @@ export class MatChipList extends _MatChipListMixinBase implements MatFormFieldCo this._dropSubscriptions(); } + /** Associates an HTML input element with this chip list. */ registerInput(inputElement: MatChipTextControl): void { this._chipInput = inputElement; @@ -495,12 +499,17 @@ export class MatChipList extends _MatChipListMixinBase implements MatFormFieldCo _keydown(event: KeyboardEvent) { const target = event.target as HTMLElement; - if (target && target.classList.contains('mat-chip')) { + // If they are on an empty input and hit backspace, focus the last chip + if (event.keyCode === BACKSPACE && this._isInputEmpty(target)) { + this._keyManager.setLastItemActive(); + event.preventDefault(); + } else if (target && target.classList.contains('mat-chip')) { this._keyManager.onKeydown(event); this.stateChanges.next(); } } + /** * Check the tab index as you should not be allowed to focus an empty list. */ @@ -537,6 +546,15 @@ export class MatChipList extends _MatChipListMixinBase implements MatFormFieldCo return index >= 0 && index < this.chips.length; } + private _isInputEmpty(element: HTMLElement): boolean { + if (element && element.nodeName.toLowerCase() === 'input') { + let input = element as HTMLInputElement; + return !input.value; + } + + return false; + } + _setSelectionByValue(value: any, isUserInput: boolean = true) { this._clearSelection(); this.chips.forEach(chip => chip.deselect()); diff --git a/tools/public_api_guard/material/chips.d.ts b/tools/public_api_guard/material/chips.d.ts index c266a2b6e4c7..3a996eb3e0f6 100644 --- a/tools/public_api_guard/material/chips.d.ts +++ b/tools/public_api_guard/material/chips.d.ts @@ -70,10 +70,11 @@ export interface MatChipEvent { chip: MatChip; } -export declare class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy, AfterContentInit { +export declare class MatChipInput implements MatChipTextControl, OnChanges { _addOnBlur: boolean; _chipList: MatChipList; protected _elementRef: ElementRef; + protected _inputElement: HTMLInputElement; get addOnBlur(): boolean; set addOnBlur(value: boolean); chipEnd: EventEmitter; @@ -83,7 +84,6 @@ export declare class MatChipInput implements MatChipTextControl, OnChanges, OnDe get empty(): boolean; focused: boolean; id: string; - readonly inputElement: HTMLInputElement; placeholder: string; separatorKeyCodes: readonly number[] | ReadonlySet; constructor(_elementRef: ElementRef, _defaultOptions: MatChipsDefaultOptions); @@ -91,13 +91,9 @@ export declare class MatChipInput implements MatChipTextControl, OnChanges, OnDe _emitChipEnd(event?: KeyboardEvent): void; _focus(): void; _keydown(event?: KeyboardEvent): void; - _keyup(event: KeyboardEvent): void; _onInput(): void; - clear(): void; focus(options?: FocusOptions): void; - ngAfterContentInit(): void; ngOnChanges(): void; - ngOnDestroy(): void; static ngAcceptInputType_addOnBlur: BooleanInput; static ngAcceptInputType_disabled: BooleanInput; static ɵdir: i0.ɵɵDirectiveDefWithMeta; @@ -105,7 +101,6 @@ export declare class MatChipInput implements MatChipTextControl, OnChanges, OnDe } export interface MatChipInputEvent { - chipInput?: MatChipInput; input: HTMLInputElement; value: string; }