diff --git a/src/lib/core/a11y/list-key-manager.spec.ts b/src/lib/core/a11y/list-key-manager.spec.ts index c0b3e28021d1..e552e2642ddd 100644 --- a/src/lib/core/a11y/list-key-manager.spec.ts +++ b/src/lib/core/a11y/list-key-manager.spec.ts @@ -48,7 +48,6 @@ describe('Key managers', () => { TAB_EVENT = new FakeEvent(TAB) as KeyboardEvent; HOME_EVENT = new FakeEvent(HOME) as KeyboardEvent; END_EVENT = new FakeEvent(END) as KeyboardEvent; - }); @@ -218,11 +217,11 @@ describe('Key managers', () => { }); it('should emit tabOut when the tab key is pressed', () => { - let tabOutEmitted = false; - keyManager.tabOut.first().subscribe(() => tabOutEmitted = true); + let spy = jasmine.createSpy('tabOut spy'); + keyManager.tabOut.first().subscribe(spy); keyManager.onKeydown(TAB_EVENT); - expect(tabOutEmitted).toBe(true); + expect(spy).toHaveBeenCalled(); }); it('should prevent the default keyboard action', () => { diff --git a/src/lib/core/a11y/list-key-manager.ts b/src/lib/core/a11y/list-key-manager.ts index d54bf1a1521d..b9e8cbb7b6b2 100644 --- a/src/lib/core/a11y/list-key-manager.ts +++ b/src/lib/core/a11y/list-key-manager.ts @@ -18,7 +18,7 @@ export interface CanDisable { export class ListKeyManager { private _activeItemIndex: number = null; private _activeItem: T; - private _tabOut: Subject = new Subject(); + private _tabOut = new Subject(); private _wrap: boolean = false; constructor(private _items: QueryList) { diff --git a/src/lib/core/overlay/overlay-directives.spec.ts b/src/lib/core/overlay/overlay-directives.spec.ts index 3af7f42e7911..8b9817cbc6ae 100644 --- a/src/lib/core/overlay/overlay-directives.spec.ts +++ b/src/lib/core/overlay/overlay-directives.spec.ts @@ -6,6 +6,8 @@ import {OverlayContainer} from './overlay-container'; import {ConnectedPositionStrategy} from './position/connected-position-strategy'; import {ConnectedOverlayPositionChange} from './position/connected-position'; import {Dir} from '../rtl/dir'; +import {dispatchKeyboardEvent} from '../testing/dispatch-events'; +import {ESCAPE} from '../keyboard/keycodes'; describe('Overlay directives', () => { @@ -98,6 +100,17 @@ describe('Overlay directives', () => { expect(getPaneElement().getAttribute('dir')).toBe('ltr'); }); + it('should close when pressing escape', () => { + fixture.componentInstance.isOpen = true; + fixture.detectChanges(); + + dispatchKeyboardEvent(document, 'keydown', ESCAPE); + fixture.detectChanges(); + + expect(overlayContainerElement.textContent.trim()).toBe('', + 'Expected overlay to have been detached.'); + }); + describe('inputs', () => { it('should set the width', () => { diff --git a/src/lib/core/overlay/overlay-directives.ts b/src/lib/core/overlay/overlay-directives.ts index e643256c11cf..5efd42f100c0 100644 --- a/src/lib/core/overlay/overlay-directives.ts +++ b/src/lib/core/overlay/overlay-directives.ts @@ -9,7 +9,8 @@ import { Input, OnDestroy, Output, - ElementRef + ElementRef, + Renderer2, } from '@angular/core'; import {Overlay, OVERLAY_PROVIDERS} from './overlay'; import {OverlayRef} from './overlay-ref'; @@ -21,10 +22,12 @@ import { } from './position/connected-position'; import {PortalModule} from '../portal/portal-directives'; import {ConnectedPositionStrategy} from './position/connected-position-strategy'; -import {Subscription} from 'rxjs/Subscription'; import {Dir, LayoutDirection} from '../rtl/dir'; import {Scrollable} from './scroll/scrollable'; import {coerceBooleanProperty} from '../coercion/boolean-property'; +import {ESCAPE} from '../keyboard/keycodes'; +import {Subscription} from 'rxjs/Subscription'; + /** Default set of positions for the overlay. Follows the behavior of a dropdown. */ let defaultPositionList = [ @@ -68,6 +71,7 @@ export class ConnectedOverlayDirective implements OnDestroy { private _offsetX: number = 0; private _offsetY: number = 0; private _position: ConnectedPositionStrategy; + private _escapeListener: Function; /** Origin for the connected overlay. */ @Input() origin: OverlayOrigin; @@ -152,6 +156,7 @@ export class ConnectedOverlayDirective implements OnDestroy { constructor( private _overlay: Overlay, + private _renderer: Renderer2, templateRef: TemplateRef, viewContainerRef: ViewContainerRef, @Optional() private _dir: Dir) { @@ -249,6 +254,7 @@ export class ConnectedOverlayDirective implements OnDestroy { this._position.withDirection(this.dir); this._overlayRef.getState().direction = this.dir; + this._initEscapeListener(); if (!this._overlayRef.hasAttached()) { this._overlayRef.attach(this._templatePortal); @@ -273,6 +279,10 @@ export class ConnectedOverlayDirective implements OnDestroy { this._backdropSubscription.unsubscribe(); this._backdropSubscription = null; } + + if (this._escapeListener) { + this._escapeListener(); + } } /** Destroys the overlay created by this directive. */ @@ -284,9 +294,23 @@ export class ConnectedOverlayDirective implements OnDestroy { if (this._backdropSubscription) { this._backdropSubscription.unsubscribe(); } + if (this._positionSubscription) { this._positionSubscription.unsubscribe(); } + + if (this._escapeListener) { + this._escapeListener(); + } + } + + /** Sets the event listener that closes the overlay when pressing Escape. */ + private _initEscapeListener() { + this._escapeListener = this._renderer.listen('document', 'keydown', (event: KeyboardEvent) => { + if (event.keyCode === ESCAPE) { + this._detachOverlay(); + } + }); } } diff --git a/src/lib/select/select.html b/src/lib/select/select.html index 9c9f0eefac97..c31a4b3f3518 100644 --- a/src/lib/select/select.html +++ b/src/lib/select/select.html @@ -15,7 +15,7 @@ + [offsetY]="_offsetY" [offsetX]="_offsetX" (attach)="_setScrollTop()" (detach)="close()">
diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index b53781ede656..0e74b9cc4eda 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -20,8 +20,9 @@ import { ControlValueAccessor, FormControl, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; import {ViewportRuler} from '../core/overlay/position/viewport-ruler'; -import {dispatchFakeEvent} from '../core/testing/dispatch-events'; +import {dispatchFakeEvent, dispatchKeyboardEvent} from '../core/testing/dispatch-events'; import {wrappedErrorMessage} from '../core/testing/wrapped-error-message'; +import {TAB} from '../core/keyboard/keycodes'; describe('MdSelect', () => { @@ -205,6 +206,20 @@ describe('MdSelect', () => { }); })); + it('should close the panel when tabbing out', async(() => { + trigger.click(); + fixture.detectChanges(); + expect(fixture.componentInstance.select.panelOpen).toBe(true); + + const panel = overlayContainerElement.querySelector('.mat-select-panel'); + dispatchKeyboardEvent(panel, 'keydown', TAB); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(fixture.componentInstance.select.panelOpen).toBe(false); + }); + })); + }); describe('selection logic', () => {