Skip to content

Commit 369ca01

Browse files
committed
fix(menu): not closed correctly if opened by different trigger while visible
Fixes the `MatMenu` overlay not being detached if the menu is opened by a different trigger while the panel is still open. Fixes #15354.
1 parent ae41a0a commit 369ca01

File tree

5 files changed

+62
-7
lines changed

5 files changed

+62
-7
lines changed

src/lib/menu/menu-directive.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,13 @@ export class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnI
240240
@Output() readonly closed: EventEmitter<void | 'click' | 'keydown' | 'tab'> =
241241
new EventEmitter<void | 'click' | 'keydown' | 'tab'>();
242242

243+
244+
/**
245+
* Stream that emits the trigger that the menu was opened by.
246+
* @docs-private
247+
*/
248+
@Output() openedBy = new EventEmitter<any>();
249+
243250
/**
244251
* Event emitted when the menu is closed.
245252
* @deprecated Switch to `closed` instead
@@ -264,6 +271,7 @@ export class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnI
264271
ngOnDestroy() {
265272
this._tabSubscription.unsubscribe();
266273
this.closed.complete();
274+
this.openedBy.complete();
267275
}
268276

269277
/** Stream that emits whenever the hovered menu item changes. */

src/lib/menu/menu-panel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export interface MatMenuPanel<T = any> {
3333
focusFirstItem: (origin?: FocusOrigin) => void;
3434
resetActiveItem: () => void;
3535
setPositionClasses?: (x: MenuPositionX, y: MenuPositionY) => void;
36+
openedBy?: EventEmitter<any>;
3637
setElevation?(depth: number): void;
3738
lazyContent?: MatMenuContent;
3839
backdropClass?: string;

src/lib/menu/menu-trigger.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import {
3434
ViewContainerRef,
3535
} from '@angular/core';
3636
import {normalizePassiveListenerOptions} from '@angular/cdk/platform';
37-
import {asapScheduler, merge, of as observableOf, Subscription} from 'rxjs';
37+
import {asapScheduler, merge, of as observableOf, Subscription, Subject} from 'rxjs';
3838
import {delay, filter, take, takeUntil} from 'rxjs/operators';
3939
import {MatMenu} from './menu-directive';
4040
import {throwMatMenuMissingError} from './menu-errors';
@@ -87,7 +87,7 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
8787
private _menuOpen: boolean = false;
8888
private _closingActionsSubscription = Subscription.EMPTY;
8989
private _hoverSubscription = Subscription.EMPTY;
90-
private _menuCloseSubscription = Subscription.EMPTY;
90+
private _menuChanged = new Subject<void>();
9191
private _scrollStrategy: () => ScrollStrategy;
9292

9393
/**
@@ -119,10 +119,18 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
119119
}
120120

121121
this._menu = menu;
122-
this._menuCloseSubscription.unsubscribe();
122+
this._menuChanged.next();
123123

124124
if (menu) {
125-
this._menuCloseSubscription = menu.close.asObservable().subscribe(reason => {
125+
if (menu.openedBy) {
126+
menu.openedBy.pipe(takeUntil(this._menuChanged)).subscribe((reason?: MatMenuTrigger) => {
127+
if (reason && reason !== this) {
128+
this._destroyMenu();
129+
}
130+
});
131+
}
132+
133+
menu.close.pipe(takeUntil(this._menuChanged)).subscribe(reason => {
126134
this._destroyMenu();
127135

128136
// If a click closed the menu, we should close the entire chain of nested menus.
@@ -203,6 +211,7 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
203211

204212
this._cleanUpSubscriptions();
205213
this._closingActionsSubscription.unsubscribe();
214+
this._menuChanged.complete();
206215
}
207216

208217
/** Whether the menu is open. */
@@ -314,11 +323,16 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
314323
* the menu was opened via the keyboard.
315324
*/
316325
private _initMenu(): void {
317-
this.menu.parentMenu = this.triggersSubmenu() ? this._parentMenu : undefined;
318-
this.menu.direction = this.dir;
326+
const menu = this.menu;
327+
menu.parentMenu = this.triggersSubmenu() ? this._parentMenu : undefined;
328+
menu.direction = this.dir;
319329
this._setMenuElevation();
320330
this._setIsMenuOpen(true);
321-
this.menu.focusFirstItem(this._openedBy || 'program');
331+
menu.focusFirstItem(this._openedBy || 'program');
332+
333+
if (menu.openedBy) {
334+
menu.openedBy.emit(this);
335+
}
322336
}
323337

324338
/** Updates the menu elevation based on the amount of parent menus that it has. */

src/lib/menu/menu.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,21 @@ describe('MatMenu', () => {
743743
flush();
744744
}));
745745

746+
it('should close the menu if it is opened by a different trigger', fakeAsync(() => {
747+
const fixture = createComponent(MenuWithMultipleTriggers);
748+
fixture.detectChanges();
749+
750+
fixture.componentInstance.triggers.first.openMenu();
751+
fixture.detectChanges();
752+
flush();
753+
expect(overlayContainerElement.querySelectorAll('.mat-menu-panel').length).toBe(1);
754+
755+
fixture.componentInstance.triggers.last.openMenu();
756+
fixture.detectChanges();
757+
flush();
758+
expect(overlayContainerElement.querySelectorAll('.mat-menu-panel').length).toBe(1);
759+
}));
760+
746761
describe('lazy rendering', () => {
747762
it('should be able to render the menu content lazily', fakeAsync(() => {
748763
const fixture = createComponent(SimpleLazyMenu);
@@ -2313,3 +2328,18 @@ class DynamicPanelMenu {
23132328
class MenuWithCheckboxItems {
23142329
@ViewChild(MatMenuTrigger) trigger: MatMenuTrigger;
23152330
}
2331+
2332+
2333+
@Component({
2334+
template: `
2335+
<button [matMenuTriggerFor]="menu">First trigger</button>
2336+
<button [matMenuTriggerFor]="menu">Second trigger</button>
2337+
<mat-menu #menu="matMenu">
2338+
<button mat-menu-item>Item one</button>
2339+
<button mat-menu-item>Item two</button>
2340+
</mat-menu>
2341+
`
2342+
})
2343+
class MenuWithMultipleTriggers {
2344+
@ViewChildren(MatMenuTrigger) triggers: QueryList<MatMenuTrigger>;
2345+
}

tools/public_api_guard/lib/menu.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export declare class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuIt
1919
hasBackdrop: boolean | undefined;
2020
items: QueryList<MatMenuItem>;
2121
lazyContent: MatMenuContent;
22+
openedBy: EventEmitter<any>;
2223
overlapTrigger: boolean;
2324
panelClass: string;
2425
parentMenu: MatMenuPanel | undefined;
@@ -90,6 +91,7 @@ export interface MatMenuPanel<T = any> {
9091
focusFirstItem: (origin?: FocusOrigin) => void;
9192
hasBackdrop?: boolean;
9293
lazyContent?: MatMenuContent;
94+
openedBy?: EventEmitter<any>;
9395
overlapTrigger: boolean;
9496
parentMenu?: MatMenuPanel | undefined;
9597
removeItem?: (item: T) => void;

0 commit comments

Comments
 (0)