From b61af4ff5f5cbe70bd579ceb46afa0f886abc63f Mon Sep 17 00:00:00 2001 From: Will Howell Date: Wed, 2 Aug 2017 21:35:41 -0400 Subject: [PATCH] fix(autocomplete): emit closing action for escape keydown event --- src/lib/autocomplete/autocomplete-trigger.ts | 8 ++- src/lib/autocomplete/autocomplete.spec.ts | 65 ++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index f1622d6c93fc..101d9f375711 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -38,6 +38,7 @@ import {MatOption, MatOptionSelectionChange} from '@angular/material/core'; import {MatFormField} from '@angular/material/form-field'; import {DOCUMENT} from '@angular/platform-browser'; import {Observable} from 'rxjs/Observable'; +import {Subject} from 'rxjs/Subject'; import {fromEvent} from 'rxjs/observable/fromEvent'; import {merge} from 'rxjs/observable/merge'; import {of as observableOf} from 'rxjs/observable/of'; @@ -125,6 +126,9 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { /** The subscription for closing actions (some are bound to document). */ private _closingActionsSubscription: Subscription; + /** Stream of escape keyboard events. */ + private _escapeEventStream = new Subject(); + /** View -> model callback called when value changes */ _onChange: (value: any) => void = () => {}; @@ -145,6 +149,7 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { ngOnDestroy() { this._destroyPanel(); + this._escapeEventStream.complete(); } /* Whether or not the autocomplete panel is open. */ @@ -186,6 +191,7 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { return merge( this.optionSelections, this.autocomplete._keyManager.tabOut, + this._escapeEventStream, this._outsideClickStream ); } @@ -262,7 +268,7 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { if (keyCode === ESCAPE && this.panelOpen) { this._resetActiveItem(); - this.closePanel(); + this._escapeEventStream.next(); event.stopPropagation(); } else if (this.activeOption && keyCode === ENTER && this.panelOpen) { this.activeOption._selectViaInteraction(); diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index aa8aafa97043..0f086b4fd1af 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -1403,6 +1403,71 @@ describe('MatAutocomplete', () => { })); }); + describe('panel closing', () => { + let fixture: ComponentFixture; + let input: HTMLInputElement; + let trigger: MatAutocompleteTrigger; + let closingActionSpy: jasmine.Spy; + let closingActionsSub: Subscription; + + beforeEach(() => { + fixture = TestBed.createComponent(SimpleAutocomplete); + fixture.detectChanges(); + + input = fixture.debugElement.query(By.css('input')).nativeElement; + + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + + trigger = fixture.componentInstance.trigger; + closingActionSpy = jasmine.createSpy('closing action listener'); + closingActionsSub = trigger.panelClosingActions.subscribe(closingActionSpy); + }); + + afterEach(() => { + closingActionsSub.unsubscribe(); + }); + + it('should emit panel close event when clicking away', async(() => { + fixture.whenStable().then(() => { + expect(closingActionSpy).not.toHaveBeenCalled(); + dispatchFakeEvent(document, 'click'); + expect(closingActionSpy).toHaveBeenCalled(); + }); + })); + + it('should emit panel close event when tabbing out', async(() => { + const tabEvent = createKeyboardEvent('keydown', TAB); + input.focus(); + + fixture.whenStable().then(() => { + expect(closingActionSpy).not.toHaveBeenCalled(); + trigger._handleKeydown(tabEvent); + expect(closingActionSpy).toHaveBeenCalled(); + }); + })); + + it('should emit panel close event when selecting an option', async(() => { + fixture.whenStable().then(() => { + const option = overlayContainerElement.querySelector('mat-option') as HTMLElement; + + expect(closingActionSpy).not.toHaveBeenCalled(); + option.click(); + expect(closingActionSpy).toHaveBeenCalled(); + }); + })); + + it('should close the panel when pressing escape', async(() => { + const escapeEvent = createKeyboardEvent('keydown', ESCAPE); + + fixture.whenStable().then(() => { + expect(closingActionSpy).not.toHaveBeenCalled(); + trigger._handleKeydown(escapeEvent); + expect(closingActionSpy).toHaveBeenCalled(); + }); + })); + }); + describe('without matInput', () => { let fixture: ComponentFixture;