From c6cd9924133a3bd8524d0937067179248d1a52ef 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 | 67 +++++++++++++++++++- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index 89e8d3487b90..90087519d1cb 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -38,6 +38,7 @@ import {MdOption, MdOptionSelectionChange} from '@angular/material/core'; import {MdFormField} 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'; @@ -127,6 +128,9 @@ export class MdAutocompleteTrigger 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 = () => {}; @@ -157,6 +161,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy { ngOnDestroy() { this._destroyPanel(); + this._escapeEventStream.complete(); } /* Whether or not the autocomplete panel is open. */ @@ -198,6 +203,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy { return merge( this.optionSelections, this.autocomplete._keyManager.tabOut, + this._escapeEventStream, this._outsideClickStream ); } @@ -272,7 +278,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy { _handleKeydown(event: KeyboardEvent): void { if (event.keyCode === ESCAPE && this.panelOpen) { this._resetActiveItem(); - this.closePanel(); + this._escapeEventStream.next(); event.stopPropagation(); } else if (this.activeOption && event.keyCode === ENTER && this.panelOpen) { this.activeOption._selectViaInteraction(); diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index e4886f42dd1d..2c24c537432d 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -1,5 +1,5 @@ import {Direction, Directionality} from '@angular/cdk/bidi'; -import {DOWN_ARROW, ENTER, ESCAPE, SPACE, UP_ARROW} from '@angular/cdk/keycodes'; +import {DOWN_ARROW, ENTER, ESCAPE, SPACE, TAB, UP_ARROW} from '@angular/cdk/keycodes'; import {OverlayContainer} from '@angular/cdk/overlay'; import {map, RxChain, startWith} from '@angular/cdk/rxjs'; import {ScrollDispatcher} from '@angular/cdk/scrolling'; @@ -1359,6 +1359,71 @@ describe('MdAutocomplete', () => { })); }); + describe('panel closing', () => { + let fixture: ComponentFixture; + let input: HTMLInputElement; + let trigger: MdAutocompleteTrigger; + 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('md-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 mdInput', () => { let fixture: ComponentFixture;