Skip to content

Commit 1cccd4b

Browse files
ppham27mmalerba
authored andcommitted
fix(menu): multiple close events for a single close (#6961)
* fix(menu): multiple close events for a single close Don't emit a closed event when another event will be emitted. Previously, if one clicked on a menu item, one would get two events: `undefined` and `click` in that order. One would see similar behavior for `keydown` or clicking the backdrop. Unit tests were updated to prevent a regression. * Responding to review comments from @crisbeto * Observable.empty() -> Observable.of() _closeMenu -> _destroyMenu
1 parent 04bf3d1 commit 1cccd4b

File tree

2 files changed

+48
-22
lines changed

2 files changed

+48
-22
lines changed

src/lib/menu/menu-trigger.ts

+17-11
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ import {MdMenuItem} from './menu-item';
4343
import {MdMenuPanel} from './menu-panel';
4444
import {MenuPositionX, MenuPositionY} from './menu-positions';
4545
import {throwMdMenuMissingError} from './menu-errors';
46-
import {of as observableOf} from 'rxjs/observable/of';
4746
import {merge} from 'rxjs/observable/merge';
47+
import {of as observableOf} from 'rxjs/observable/of';
4848
import {Subscription} from 'rxjs/Subscription';
4949

5050
/** Injection token that determines the scroll handling while the menu is open. */
@@ -153,7 +153,7 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
153153
this._checkMenu();
154154

155155
this.menu.close.subscribe(reason => {
156-
this.closeMenu();
156+
this._destroyMenu();
157157

158158
// If a click closed the menu, we should close the entire chain of nested menus.
159159
if (reason === 'click' && this._parentMenu) {
@@ -205,7 +205,9 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
205205
openMenu(): void {
206206
if (!this._menuOpen) {
207207
this._createOverlay().attach(this._portal);
208-
this._closeSubscription = this._menuClosingActions().subscribe(() => this.menu.close.emit());
208+
this._closeSubscription = this._menuClosingActions().subscribe(() => {
209+
this.menu.close.emit();
210+
});
209211
this._initMenu();
210212

211213
if (this.menu instanceof MdMenu) {
@@ -216,23 +218,27 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
216218

217219
/** Closes the menu. */
218220
closeMenu(): void {
221+
this.menu.close.emit();
222+
}
223+
224+
/** Focuses the menu trigger. */
225+
focus() {
226+
this._element.nativeElement.focus();
227+
}
228+
229+
/** Closes the menu and does the necessary cleanup. */
230+
private _destroyMenu() {
219231
if (this._overlayRef && this.menuOpen) {
220232
this._resetMenu();
221233
this._overlayRef.detach();
222234
this._closeSubscription.unsubscribe();
223-
this.menu.close.emit();
224235

225236
if (this.menu instanceof MdMenu) {
226237
this.menu._resetAnimation();
227238
}
228239
}
229240
}
230241

231-
/** Focuses the menu trigger. */
232-
focus() {
233-
this._element.nativeElement.focus();
234-
}
235-
236242
/**
237243
* This method sets the menu state to open and focuses the first item if
238244
* the menu was opened via the keyboard.
@@ -400,11 +406,11 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
400406
/** Returns a stream that emits whenever an action that should close the menu occurs. */
401407
private _menuClosingActions() {
402408
const backdrop = this._overlayRef!.backdropClick();
403-
const parentClose = this._parentMenu ? this._parentMenu.close : observableOf(null);
409+
const parentClose = this._parentMenu ? this._parentMenu.close : observableOf();
404410
const hover = this._parentMenu ? RxChain.from(this._parentMenu.hover())
405411
.call(filter, active => active !== this._menuItemInstance)
406412
.call(filter, () => this._menuOpen)
407-
.result() : observableOf(null);
413+
.result() : observableOf();
408414

409415
return merge(backdrop, parentClose, hover);
410416
}

src/lib/menu/menu.spec.ts

+31-11
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ describe('MdMenu', () => {
9898
expect(overlayContainerElement.textContent).toBe('');
9999
}));
100100

101-
it('should close the menu when pressing escape', fakeAsync(() => {
101+
it('should close the menu when pressing ESCAPE', fakeAsync(() => {
102102
const fixture = TestBed.createComponent(SimpleMenu);
103103
fixture.detectChanges();
104104
fixture.componentInstance.trigger.openMenu();
@@ -494,26 +494,40 @@ describe('MdMenu', () => {
494494
menuItem.click();
495495
fixture.detectChanges();
496496

497-
expect(fixture.componentInstance.closeCallback).toHaveBeenCalled();
497+
expect(fixture.componentInstance.closeCallback).toHaveBeenCalledWith('click');
498+
expect(fixture.componentInstance.closeCallback).toHaveBeenCalledTimes(1);
498499
});
499500

500501
it('should emit a close event when the backdrop is clicked', () => {
501-
const backdrop = <HTMLElement>overlayContainerElement.querySelector('.cdk-overlay-backdrop');
502+
const backdrop = overlayContainerElement
503+
.querySelector('.cdk-overlay-backdrop') as HTMLElement;
502504

503505
backdrop.click();
504506
fixture.detectChanges();
505507

506-
expect(fixture.componentInstance.closeCallback).toHaveBeenCalled();
508+
expect(fixture.componentInstance.closeCallback).toHaveBeenCalledWith(undefined);
509+
expect(fixture.componentInstance.closeCallback).toHaveBeenCalledTimes(1);
510+
});
511+
512+
it('should emit an event when pressing ESCAPE', () => {
513+
const menu = overlayContainerElement.querySelector('.mat-menu-panel') as HTMLElement;
514+
515+
dispatchKeyboardEvent(menu, 'keydown', ESCAPE);
516+
fixture.detectChanges();
517+
518+
expect(fixture.componentInstance.closeCallback).toHaveBeenCalledWith('keydown');
519+
expect(fixture.componentInstance.closeCallback).toHaveBeenCalledTimes(1);
507520
});
508521

509522
it('should complete the callback when the menu is destroyed', () => {
510-
let emitCallback = jasmine.createSpy('emit callback');
511-
let completeCallback = jasmine.createSpy('complete callback');
523+
const emitCallback = jasmine.createSpy('emit callback');
524+
const completeCallback = jasmine.createSpy('complete callback');
512525

513526
fixture.componentInstance.menu.close.subscribe(emitCallback, null, completeCallback);
514527
fixture.destroy();
515528

516-
expect(emitCallback).toHaveBeenCalled();
529+
expect(emitCallback).toHaveBeenCalledWith(undefined);
530+
expect(emitCallback).toHaveBeenCalledTimes(1);
517531
expect(completeCallback).toHaveBeenCalled();
518532
});
519533
});
@@ -995,6 +1009,9 @@ describe('MdMenu', () => {
9951009
tick(500);
9961010

9971011
expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(0, 'Expected no open menus');
1012+
expect(instance.rootCloseCallback).toHaveBeenCalledTimes(1);
1013+
expect(instance.levelOneCloseCallback).toHaveBeenCalledTimes(1);
1014+
expect(instance.levelTwoCloseCallback).toHaveBeenCalledTimes(1);
9981015
}));
9991016

10001017
it('should toggle a nested menu when its trigger is added after init', fakeAsync(() => {
@@ -1059,7 +1076,7 @@ describe('MdMenu default overrides', () => {
10591076
@Component({
10601077
template: `
10611078
<button [mdMenuTriggerFor]="menu" #triggerEl>Toggle menu</button>
1062-
<md-menu class="custom-one custom-two" #menu="mdMenu" (close)="closeCallback()">
1079+
<md-menu class="custom-one custom-two" #menu="mdMenu" (close)="closeCallback($event)">
10631080
<button md-menu-item> Item </button>
10641081
<button md-menu-item disabled> Disabled </button>
10651082
</md-menu>
@@ -1152,7 +1169,7 @@ class CustomMenu {
11521169
[mdMenuTriggerFor]="levelTwo"
11531170
#alternateTrigger="mdMenuTrigger">Toggle alternate menu</button>
11541171
1155-
<md-menu #root="mdMenu">
1172+
<md-menu #root="mdMenu" (close)="rootCloseCallback($event)">
11561173
<button md-menu-item
11571174
id="level-one-trigger"
11581175
[mdMenuTriggerFor]="levelOne"
@@ -1165,7 +1182,7 @@ class CustomMenu {
11651182
#lazyTrigger="mdMenuTrigger">Three</button>
11661183
</md-menu>
11671184
1168-
<md-menu #levelOne="mdMenu">
1185+
<md-menu #levelOne="mdMenu" (close)="levelOneCloseCallback($event)">
11691186
<button md-menu-item>Four</button>
11701187
<button md-menu-item
11711188
id="level-two-trigger"
@@ -1174,7 +1191,7 @@ class CustomMenu {
11741191
<button md-menu-item>Six</button>
11751192
</md-menu>
11761193
1177-
<md-menu #levelTwo="mdMenu">
1194+
<md-menu #levelTwo="mdMenu" (close)="levelTwoCloseCallback($event)">
11781195
<button md-menu-item>Seven</button>
11791196
<button md-menu-item>Eight</button>
11801197
<button md-menu-item>Nine</button>
@@ -1192,12 +1209,15 @@ class NestedMenu {
11921209
@ViewChild('rootTrigger') rootTrigger: MdMenuTrigger;
11931210
@ViewChild('rootTriggerEl') rootTriggerEl: ElementRef;
11941211
@ViewChild('alternateTrigger') alternateTrigger: MdMenuTrigger;
1212+
readonly rootCloseCallback = jasmine.createSpy('root menu closed callback');
11951213

11961214
@ViewChild('levelOne') levelOneMenu: MdMenu;
11971215
@ViewChild('levelOneTrigger') levelOneTrigger: MdMenuTrigger;
1216+
readonly levelOneCloseCallback = jasmine.createSpy('level one menu closed callback');
11981217

11991218
@ViewChild('levelTwo') levelTwoMenu: MdMenu;
12001219
@ViewChild('levelTwoTrigger') levelTwoTrigger: MdMenuTrigger;
1220+
readonly levelTwoCloseCallback = jasmine.createSpy('level one menu closed callback');
12011221

12021222
@ViewChild('lazy') lazyMenu: MdMenu;
12031223
@ViewChild('lazyTrigger') lazyTrigger: MdMenuTrigger;

0 commit comments

Comments
 (0)