Skip to content

Commit 2dcb76c

Browse files
ppham27andrewseguin
authored andcommitted
fix(menu): multiple close events for a single close (#7037)
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.
1 parent b0b91f4 commit 2dcb76c

File tree

2 files changed

+47
-22
lines changed

2 files changed

+47
-22
lines changed

src/lib/menu/menu-trigger.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ import {MatMenuItem} from './menu-item';
4444
import {MatMenuPanel} from './menu-panel';
4545
import {MenuPositionX, MenuPositionY} from './menu-positions';
4646

47-
4847
/** Injection token that determines the scroll handling while the menu is open. */
4948
export const MAT_MENU_SCROLL_STRATEGY =
5049
new InjectionToken<() => ScrollStrategy>('mat-menu-scroll-strategy');
@@ -130,7 +129,7 @@ export class MatMenuTrigger implements AfterViewInit, OnDestroy {
130129
this._checkMenu();
131130

132131
this.menu.close.subscribe(reason => {
133-
this.closeMenu();
132+
this._destroyMenu();
134133

135134
// If a click closed the menu, we should close the entire chain of nested menus.
136135
if (reason === 'click' && this._parentMenu) {
@@ -182,7 +181,9 @@ export class MatMenuTrigger implements AfterViewInit, OnDestroy {
182181
openMenu(): void {
183182
if (!this._menuOpen) {
184183
this._createOverlay().attach(this._portal);
185-
this._closeSubscription = this._menuClosingActions().subscribe(() => this.menu.close.emit());
184+
this._closeSubscription = this._menuClosingActions().subscribe(() => {
185+
this.menu.close.emit();
186+
});
186187
this._initMenu();
187188

188189
if (this.menu instanceof MatMenu) {
@@ -193,23 +194,27 @@ export class MatMenuTrigger implements AfterViewInit, OnDestroy {
193194

194195
/** Closes the menu. */
195196
closeMenu(): void {
197+
this.menu.close.emit();
198+
}
199+
200+
/** Focuses the menu trigger. */
201+
focus() {
202+
this._element.nativeElement.focus();
203+
}
204+
205+
/** Closes the menu and does the necessary cleanup. */
206+
private _destroyMenu() {
196207
if (this._overlayRef && this.menuOpen) {
197208
this._resetMenu();
198209
this._overlayRef.detach();
199210
this._closeSubscription.unsubscribe();
200-
this.menu.close.emit();
201211

202212
if (this.menu instanceof MatMenu) {
203213
this.menu._resetAnimation();
204214
}
205215
}
206216
}
207217

208-
/** Focuses the menu trigger. */
209-
focus() {
210-
this._element.nativeElement.focus();
211-
}
212-
213218
/**
214219
* This method sets the menu state to open and focuses the first item if
215220
* the menu was opened via the keyboard.
@@ -377,11 +382,11 @@ export class MatMenuTrigger implements AfterViewInit, OnDestroy {
377382
/** Returns a stream that emits whenever an action that should close the menu occurs. */
378383
private _menuClosingActions() {
379384
const backdrop = this._overlayRef!.backdropClick();
380-
const parentClose = this._parentMenu ? this._parentMenu.close : observableOf(null);
385+
const parentClose = this._parentMenu ? this._parentMenu.close : observableOf();
381386
const hover = this._parentMenu ? RxChain.from(this._parentMenu.hover())
382387
.call(filter, active => active !== this._menuItemInstance)
383388
.call(filter, () => this._menuOpen)
384-
.result() : observableOf(null);
389+
.result() : observableOf();
385390

386391
return merge(backdrop, parentClose, hover);
387392
}

src/lib/menu/menu.spec.ts

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ describe('MatMenu', () => {
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('MatMenu', () => {
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('MatMenu', () => {
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('MatMenu default overrides', () => {
10591076
@Component({
10601077
template: `
10611078
<button [matMenuTriggerFor]="menu" #triggerEl>Toggle menu</button>
1062-
<mat-menu class="custom-one custom-two" #menu="matMenu" (close)="closeCallback()">
1079+
<mat-menu class="custom-one custom-two" #menu="matMenu" (close)="closeCallback($event)">
10631080
<button mat-menu-item> Item </button>
10641081
<button mat-menu-item disabled> Disabled </button>
10651082
</mat-menu>
@@ -1152,7 +1169,7 @@ class CustomMenu {
11521169
[matMenuTriggerFor]="levelTwo"
11531170
#alternateTrigger="matMenuTrigger">Toggle alternate menu</button>
11541171
1155-
<mat-menu #root="matMenu">
1172+
<mat-menu #root="matMenu" (close)="rootCloseCallback($event)">
11561173
<button mat-menu-item
11571174
id="level-one-trigger"
11581175
[matMenuTriggerFor]="levelOne"
@@ -1165,7 +1182,7 @@ class CustomMenu {
11651182
#lazyTrigger="matMenuTrigger">Three</button>
11661183
</mat-menu>
11671184
1168-
<mat-menu #levelOne="matMenu">
1185+
<mat-menu #levelOne="matMenu" (close)="levelOneCloseCallback($event)">
11691186
<button mat-menu-item>Four</button>
11701187
<button mat-menu-item
11711188
id="level-two-trigger"
@@ -1174,7 +1191,7 @@ class CustomMenu {
11741191
<button mat-menu-item>Six</button>
11751192
</mat-menu>
11761193
1177-
<mat-menu #levelTwo="matMenu">
1194+
<mat-menu #levelTwo="matMenu" (close)="levelTwoCloseCallback($event)">
11781195
<button mat-menu-item>Seven</button>
11791196
<button mat-menu-item>Eight</button>
11801197
<button mat-menu-item>Nine</button>
@@ -1192,12 +1209,15 @@ class NestedMenu {
11921209
@ViewChild('rootTrigger') rootTrigger: MatMenuTrigger;
11931210
@ViewChild('rootTriggerEl') rootTriggerEl: ElementRef;
11941211
@ViewChild('alternateTrigger') alternateTrigger: MatMenuTrigger;
1212+
readonly rootCloseCallback = jasmine.createSpy('root menu closed callback');
11951213

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

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

12021222
@ViewChild('lazy') lazyMenu: MatMenu;
12031223
@ViewChild('lazyTrigger') lazyTrigger: MatMenuTrigger;

0 commit comments

Comments
 (0)