Skip to content

feat(menu): add disableClose option #4842

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 22 additions & 10 deletions src/lib/menu/menu-directive.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// TODO(kara): prevent-close functionality

import {
AfterContentInit,
Component,
Expand All @@ -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({
Expand Down Expand Up @@ -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
Expand All @@ -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() {
Expand All @@ -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();
}
}

Expand Down
1 change: 1 addition & 0 deletions src/lib/menu/menu-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface MdMenuPanel {
overlapTrigger: boolean;
templateRef: TemplateRef<any>;
close: EventEmitter<void>;
disableClose: boolean;
focusFirstItem: () => void;
setPositionClasses: (x: MenuPositionX, y: MenuPositionY) => void;
_emitCloseEvent: () => void;
Expand Down
13 changes: 9 additions & 4 deletions src/lib/menu/menu-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
}

Expand Down Expand Up @@ -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();
});
}
}

/**
Expand Down
3 changes: 1 addition & 2 deletions src/lib/menu/menu.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
<ng-template>
<div class="mat-menu-panel" [ngClass]="_classList" (keydown)="_handleKeydown($event)"
(click)="_emitCloseEvent()" [@transformMenu]="'showing'">
(click)="_handleClick()" [@transformMenu]="'showing'">
<div class="mat-menu-content" [@fadeInItems]="'showing'">
<ng-content></ng-content>
</div>
</div>
</ng-template>

66 changes: 63 additions & 3 deletions src/lib/menu/menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';


Expand Down Expand Up @@ -457,12 +457,70 @@ describe('MdMenu', () => {
expect(fixture.destroy.bind(fixture)).not.toThrow();
});
});

describe('disableClose', () => {
let fixture: ComponentFixture<SimpleMenu>;

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: `
<button [mdMenuTriggerFor]="menu" #triggerEl>Toggle menu</button>
<md-menu #menu="mdMenu" (close)="closeCallback()">
<md-menu #menu="mdMenu" (close)="closeCallback()" [disableClose]="disableClose">
<button md-menu-item> Item </button>
<button md-menu-item disabled> Disabled </button>
</md-menu>
Expand All @@ -472,6 +530,7 @@ class SimpleMenu {
@ViewChild(MdMenuTrigger) trigger: MdMenuTrigger;
@ViewChild('triggerEl') triggerEl: ElementRef;
closeCallback = jasmine.createSpy('menu closed callback');
disableClose = false;
}

@Component({
Expand Down Expand Up @@ -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<any>;
@Output() close = new EventEmitter<void>();
Expand Down