diff --git a/src/lib/menu/menu-directive.ts b/src/lib/menu/menu-directive.ts index e4edc8d6e801..b611f415fe4c 100644 --- a/src/lib/menu/menu-directive.ts +++ b/src/lib/menu/menu-directive.ts @@ -1,5 +1,3 @@ -// TODO(kara): prevent-close functionality - import { AfterContentInit, Component, @@ -20,7 +18,7 @@ import {FocusKeyManager} from '../core/a11y/focus-key-manager'; import {MdMenuPanel} from './menu-panel'; import {Subscription} from 'rxjs/Subscription'; import {transformMenu, fadeInItems} from './menu-animations'; -import {ESCAPE} from '../core/keyboard/keycodes'; +import {ESCAPE, coerceBooleanProperty} from '../core'; @Component({ @@ -77,6 +75,12 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy { /** Whether the menu should overlap its trigger. */ @Input() overlapTrigger = true; + /** Whether the user should be able to close the menu. */ + @Input() + get disableClose(): boolean { return this._disableClose; } + set disableClose(value: boolean) { this._disableClose = coerceBooleanProperty(value); } + private _disableClose: boolean = false; + /** * This method takes classes set on the host md-menu element and applies them on the * menu template that displays in the overlay container. Otherwise, it's difficult @@ -97,7 +101,10 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy { ngAfterContentInit() { this._keyManager = new FocusKeyManager(this.items).withWrap(); - this._tabSubscription = this._keyManager.tabOut.subscribe(() => this._emitCloseEvent()); + + if (!this._disableClose) { + this._tabSubscription = this._keyManager.tabOut.subscribe(() => this._emitCloseEvent()); + } } ngOnDestroy() { @@ -108,12 +115,17 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy { /** Handle a keyboard event from the menu, delegating to the appropriate action. */ _handleKeydown(event: KeyboardEvent) { - switch (event.keyCode) { - case ESCAPE: - this._emitCloseEvent(); - return; - default: - this._keyManager.onKeydown(event); + if (event.keyCode === ESCAPE && !this._disableClose) { + this._emitCloseEvent(); + } else { + this._keyManager.onKeydown(event); + } + } + + /** Handles clicks inside the menu panel. */ + _handleClick() { + if (!this._disableClose) { + this._emitCloseEvent(); } } diff --git a/src/lib/menu/menu-panel.ts b/src/lib/menu/menu-panel.ts index 82fab04e19d5..4a0b232c4055 100644 --- a/src/lib/menu/menu-panel.ts +++ b/src/lib/menu/menu-panel.ts @@ -7,6 +7,7 @@ export interface MdMenuPanel { overlapTrigger: boolean; templateRef: TemplateRef; close: EventEmitter; + disableClose: boolean; focusFirstItem: () => void; setPositionClasses: (x: MenuPositionX, y: MenuPositionY) => void; _emitCloseEvent: () => void; diff --git a/src/lib/menu/menu-trigger.ts b/src/lib/menu/menu-trigger.ts index 22e5240d72d0..11b8fe69118f 100644 --- a/src/lib/menu/menu-trigger.ts +++ b/src/lib/menu/menu-trigger.ts @@ -112,8 +112,11 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy { closeMenu(): void { if (this._overlayRef) { this._overlayRef.detach(); - this._backdropSubscription.unsubscribe(); this._resetMenu(); + + if (this._backdropSubscription) { + this._backdropSubscription.unsubscribe(); + } } } @@ -144,9 +147,11 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy { * explicitly when the menu is closed or destroyed. */ private _subscribeToBackdrop(): void { - this._backdropSubscription = this._overlayRef.backdropClick().subscribe(() => { - this.menu._emitCloseEvent(); - }); + if (!this.menu.disableClose) { + this._backdropSubscription = this._overlayRef.backdropClick().subscribe(() => { + this.menu._emitCloseEvent(); + }); + } } /** diff --git a/src/lib/menu/menu.html b/src/lib/menu/menu.html index 10972059e8c5..4e8839130a10 100644 --- a/src/lib/menu/menu.html +++ b/src/lib/menu/menu.html @@ -1,9 +1,8 @@
+ (click)="_handleClick()" [@transformMenu]="'showing'">
- diff --git a/src/lib/menu/menu.spec.ts b/src/lib/menu/menu.spec.ts index 18e327753363..34145e9ad4f2 100644 --- a/src/lib/menu/menu.spec.ts +++ b/src/lib/menu/menu.spec.ts @@ -20,7 +20,7 @@ import { import {OverlayContainer} from '../core/overlay/overlay-container'; import {Dir, LayoutDirection} from '../core/rtl/dir'; import {extendObject} from '../core/util/object-extend'; -import {ESCAPE} from '../core/keyboard/keycodes'; +import {ESCAPE, TAB} from '../core/keyboard/keycodes'; import {dispatchKeyboardEvent} from '../core/testing/dispatch-events'; @@ -457,12 +457,70 @@ describe('MdMenu', () => { expect(fixture.destroy.bind(fixture)).not.toThrow(); }); }); + + describe('disableClose', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + fixture = TestBed.createComponent(SimpleMenu); + fixture.componentInstance.disableClose = true; + fixture.detectChanges(); + fixture.componentInstance.trigger.openMenu(); + }); + + it('should not close when pressing ESCAPE', () => { + const panel = overlayContainerElement.querySelector('.mat-menu-panel'); + + dispatchKeyboardEvent(panel, 'keydown', ESCAPE); + fixture.detectChanges(); + + expect(overlayContainerElement.querySelector('.mat-menu-panel')).toBeTruthy(); + }); + + it('should not close when pressing TAB', () => { + const panel = overlayContainerElement.querySelector('.mat-menu-panel'); + + dispatchKeyboardEvent(panel, 'keydown', TAB); + fixture.detectChanges(); + + expect(overlayContainerElement.querySelector('.mat-menu-panel')).toBeTruthy(); + }); + + it('should not close when clicking on the backdrop', () => { + const backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop'); + + (backdrop as HTMLElement).click(); + fixture.detectChanges(); + + expect(overlayContainerElement.querySelector('.mat-menu-panel')).toBeTruthy(); + }); + + it('should not close when clicking inside the panel', () => { + const panel = overlayContainerElement.querySelector('.mat-menu-panel') as HTMLElement; + + panel.click(); + fixture.detectChanges(); + + expect(overlayContainerElement.querySelector('.mat-menu-panel')).toBeTruthy(); + }); + + it('should still be able to close programmatically', () => { + expect(overlayContainerElement.querySelector('.mat-menu-panel')).toBeTruthy(); + + fixture.componentInstance.trigger.closeMenu(); + fixture.detectChanges(); + + expect(overlayContainerElement.querySelector('.mat-menu-panel')).toBeFalsy(); + }); + + }); + }); @Component({ template: ` - + @@ -472,6 +530,7 @@ class SimpleMenu { @ViewChild(MdMenuTrigger) trigger: MdMenuTrigger; @ViewChild('triggerEl') triggerEl: ElementRef; closeCallback = jasmine.createSpy('menu closed callback'); + disableClose = false; } @Component({ @@ -520,7 +579,8 @@ class OverlapMenu implements TestableMenu { class CustomMenuPanel implements MdMenuPanel { xPosition: MenuPositionX = 'after'; yPosition: MenuPositionY = 'below'; - overlapTrigger: true; + overlapTrigger = true; + disableClose = false; @ViewChild(TemplateRef) templateRef: TemplateRef; @Output() close = new EventEmitter();