Skip to content

Commit 949bd88

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 036729d commit 949bd88

File tree

5 files changed

+61
-8
lines changed

5 files changed

+61
-8
lines changed

src/material/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/material/menu/menu-trigger.ts

Lines changed: 21 additions & 8 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';
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.
@@ -201,9 +209,9 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
201209
this._element.nativeElement.removeEventListener('touchstart', this._handleTouchStart,
202210
passiveEventListenerOptions);
203211

204-
this._menuCloseSubscription.unsubscribe();
205212
this._closingActionsSubscription.unsubscribe();
206213
this._hoverSubscription.unsubscribe();
214+
this._menuChanged.complete();
207215
}
208216

209217
/** Whether the menu is open. */
@@ -316,11 +324,16 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
316324
* the menu was opened via the keyboard.
317325
*/
318326
private _initMenu(): void {
319-
this.menu.parentMenu = this.triggersSubmenu() ? this._parentMenu : undefined;
320-
this.menu.direction = this.dir;
327+
const menu = this.menu;
328+
menu.parentMenu = this.triggersSubmenu() ? this._parentMenu : undefined;
329+
menu.direction = this.dir;
321330
this._setMenuElevation();
322331
this._setIsMenuOpen(true);
323-
this.menu.focusFirstItem(this._openedBy || 'program');
332+
menu.focusFirstItem(this._openedBy || 'program');
333+
334+
if (menu.openedBy) {
335+
menu.openedBy.emit(this);
336+
}
324337
}
325338

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

src/material/menu/menu.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,21 @@ describe('MatMenu', () => {
762762
flush();
763763
}));
764764

765+
it('should close the menu if it is opened by a different trigger', fakeAsync(() => {
766+
const fixture = createComponent(MenuWithMultipleTriggers);
767+
fixture.detectChanges();
768+
769+
fixture.componentInstance.triggers.first.openMenu();
770+
fixture.detectChanges();
771+
flush();
772+
expect(overlayContainerElement.querySelectorAll('.mat-menu-panel').length).toBe(1);
773+
774+
fixture.componentInstance.triggers.last.openMenu();
775+
fixture.detectChanges();
776+
flush();
777+
expect(overlayContainerElement.querySelectorAll('.mat-menu-panel').length).toBe(1);
778+
}));
779+
765780
describe('lazy rendering', () => {
766781
it('should be able to render the menu content lazily', fakeAsync(() => {
767782
const fixture = createComponent(SimpleLazyMenu);
@@ -2415,3 +2430,17 @@ class LazyMenuWithOnPush {
24152430
@ViewChild('triggerEl', {static: false, read: ElementRef}) rootTrigger: ElementRef;
24162431
@ViewChild('menuItem', {static: false, read: ElementRef}) menuItemWithSubmenu: ElementRef;
24172432
}
2433+
2434+
@Component({
2435+
template: `
2436+
<button [matMenuTriggerFor]="menu">First trigger</button>
2437+
<button [matMenuTriggerFor]="menu">Second trigger</button>
2438+
<mat-menu #menu="matMenu">
2439+
<button mat-menu-item>Item one</button>
2440+
<button mat-menu-item>Item two</button>
2441+
</mat-menu>
2442+
`
2443+
})
2444+
class MenuWithMultipleTriggers {
2445+
@ViewChildren(MatMenuTrigger) triggers: QueryList<MatMenuTrigger>;
2446+
}

src/material/menu/menu.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,13 @@ export class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatMenuItem>
225225
@Output() readonly closed: EventEmitter<void | 'click' | 'keydown' | 'tab'> =
226226
new EventEmitter<void | 'click' | 'keydown' | 'tab'>();
227227

228+
229+
/**
230+
* Stream that emits the trigger that the menu was opened by.
231+
* @docs-private
232+
*/
233+
@Output() openedBy = new EventEmitter<any>();
234+
228235
/**
229236
* Event emitted when the menu is closed.
230237
* @deprecated Switch to `closed` instead
@@ -251,6 +258,7 @@ export class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatMenuItem>
251258
this._directDescendantItems.destroy();
252259
this._tabSubscription.unsubscribe();
253260
this.closed.complete();
261+
this.openedBy.complete();
254262
}
255263

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

tools/public_api_guard/material/menu.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export declare class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatM
1818
hasBackdrop: boolean | undefined;
1919
items: QueryList<MatMenuItem>;
2020
lazyContent: MatMenuContent;
21+
openedBy: EventEmitter<any>;
2122
overlapTrigger: boolean;
2223
panelClass: string;
2324
parentMenu: MatMenuPanel | undefined;
@@ -104,6 +105,7 @@ export interface MatMenuPanel<T = any> {
104105
focusFirstItem: (origin?: FocusOrigin) => void;
105106
hasBackdrop?: boolean;
106107
lazyContent?: MatMenuContent;
108+
openedBy?: EventEmitter<any>;
107109
overlapTrigger: boolean;
108110
parentMenu?: MatMenuPanel | undefined;
109111
removeItem?: (item: T) => void;

0 commit comments

Comments
 (0)