Skip to content

Commit 06943e1

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 447f2e6 commit 06943e1

File tree

5 files changed

+63
-9
lines changed

5 files changed

+63
-9
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';
@@ -86,7 +86,7 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
8686
private _menuOpen: boolean = false;
8787
private _closingActionsSubscription = Subscription.EMPTY;
8888
private _hoverSubscription = Subscription.EMPTY;
89-
private _menuCloseSubscription = Subscription.EMPTY;
89+
private _menuChanged = new Subject<void>();
9090
private _scrollStrategy: () => ScrollStrategy;
9191

9292
/**
@@ -118,10 +118,18 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
118118
}
119119

120120
this._menu = menu;
121-
this._menuCloseSubscription.unsubscribe();
121+
this._menuChanged.next();
122122

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

127135
// If a click closed the menu, we should close the entire chain of nested menus.
@@ -200,9 +208,9 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
200208
this._element.nativeElement.removeEventListener('touchstart', this._handleTouchStart,
201209
passiveEventListenerOptions);
202210

203-
this._menuCloseSubscription.unsubscribe();
204211
this._closingActionsSubscription.unsubscribe();
205212
this._hoverSubscription.unsubscribe();
213+
this._menuChanged.complete();
206214
}
207215

208216
/** Whether the menu is open. */
@@ -315,11 +323,16 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
315323
* the menu was opened via the keyboard.
316324
*/
317325
private _initMenu(): void {
318-
this.menu.parentMenu = this.triggersSubmenu() ? this._parentMenu : undefined;
319-
this.menu.direction = this.dir;
326+
const menu = this.menu;
327+
menu.parentMenu = this.triggersSubmenu() ? this._parentMenu : undefined;
328+
menu.direction = this.dir;
320329
this._setMenuElevation();
321330
this._setIsMenuOpen(true);
322-
this.menu.focusFirstItem(this._openedBy || 'program');
331+
menu.focusFirstItem(this._openedBy || 'program');
332+
333+
if (menu.openedBy) {
334+
menu.openedBy.emit(this);
335+
}
323336
}
324337

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

src/material/menu/menu.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,22 @@ describe('MatMenu', () => {
866866
expect(document.activeElement).toBe(overlayContainerElement.querySelector('.mat-menu-panel'));
867867
}));
868868

869+
it('should close the menu if it is opened by a different trigger', fakeAsync(() => {
870+
const fixture = createComponent(MenuWithMultipleTriggers);
871+
fixture.detectChanges();
872+
873+
fixture.componentInstance.triggers.first.openMenu();
874+
fixture.detectChanges();
875+
flush();
876+
expect(overlayContainerElement.querySelectorAll('.mat-menu-panel').length).toBe(1);
877+
878+
fixture.componentInstance.triggers.last.openMenu();
879+
fixture.detectChanges();
880+
flush();
881+
expect(overlayContainerElement.querySelectorAll('.mat-menu-panel').length).toBe(1);
882+
883+
}));
884+
869885
describe('lazy rendering', () => {
870886
it('should be able to render the menu content lazily', fakeAsync(() => {
871887
const fixture = createComponent(SimpleLazyMenu);
@@ -2590,3 +2606,17 @@ class LazyMenuWithOnPush {
25902606
@ViewChild('triggerEl', {read: ElementRef}) rootTrigger: ElementRef;
25912607
@ViewChild('menuItem', {read: ElementRef}) menuItemWithSubmenu: ElementRef;
25922608
}
2609+
2610+
@Component({
2611+
template: `
2612+
<button [matMenuTriggerFor]="menu">First trigger</button>
2613+
<button [matMenuTriggerFor]="menu">Second trigger</button>
2614+
<mat-menu #menu="matMenu">
2615+
<button mat-menu-item>Item one</button>
2616+
<button mat-menu-item>Item two</button>
2617+
</mat-menu>
2618+
`
2619+
})
2620+
class MenuWithMultipleTriggers {
2621+
@ViewChildren(MatMenuTrigger) triggers: QueryList<MatMenuTrigger>;
2622+
}

src/material/menu/menu.ts

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

241+
242+
/**
243+
* Stream that emits the trigger that the menu was opened by.
244+
* @docs-private
245+
*/
246+
@Output() openedBy = new EventEmitter<any>();
247+
241248
/**
242249
* Event emitted when the menu is closed.
243250
* @deprecated Switch to `closed` instead
@@ -274,6 +281,7 @@ export class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatMenuItem>
274281
this._directDescendantItems.destroy();
275282
this._tabSubscription.unsubscribe();
276283
this.closed.complete();
284+
this.openedBy.complete();
277285
}
278286

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

tools/public_api_guard/material/menu.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export declare class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatM
2525
set hasBackdrop(value: boolean | undefined);
2626
items: QueryList<MatMenuItem>;
2727
lazyContent: MatMenuContent;
28+
openedBy: EventEmitter<any>;
2829
get overlapTrigger(): boolean;
2930
set overlapTrigger(value: boolean);
3031
set panelClass(classes: string);
@@ -53,7 +54,7 @@ export declare class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatM
5354
setPositionClasses(posX?: MenuPositionX, posY?: MenuPositionY): void;
5455
static ngAcceptInputType_hasBackdrop: BooleanInput;
5556
static ngAcceptInputType_overlapTrigger: BooleanInput;
56-
static ɵdir: i0.ɵɵDirectiveDefWithMeta<_MatMenuBase, never, never, { "backdropClass": "backdropClass"; "ariaLabel": "aria-label"; "ariaLabelledby": "aria-labelledby"; "ariaDescribedby": "aria-describedby"; "xPosition": "xPosition"; "yPosition": "yPosition"; "overlapTrigger": "overlapTrigger"; "hasBackdrop": "hasBackdrop"; "panelClass": "class"; "classList": "classList"; }, { "closed": "closed"; "close": "close"; }, ["lazyContent", "_allItems", "items"]>;
57+
static ɵdir: i0.ɵɵDirectiveDefWithMeta<_MatMenuBase, never, never, { "backdropClass": "backdropClass"; "ariaLabel": "aria-label"; "ariaLabelledby": "aria-labelledby"; "ariaDescribedby": "aria-describedby"; "xPosition": "xPosition"; "yPosition": "yPosition"; "overlapTrigger": "overlapTrigger"; "hasBackdrop": "hasBackdrop"; "panelClass": "class"; "classList": "classList"; }, { "closed": "closed"; "openedBy": "openedBy"; "close": "close"; }, ["lazyContent", "_allItems", "items"]>;
5758
static ɵfac: i0.ɵɵFactoryDef<_MatMenuBase, never>;
5859
}
5960

@@ -132,6 +133,7 @@ export interface MatMenuPanel<T = any> {
132133
focusFirstItem: (origin?: FocusOrigin) => void;
133134
hasBackdrop?: boolean;
134135
lazyContent?: MatMenuContent;
136+
openedBy?: EventEmitter<any>;
135137
overlapTrigger: boolean;
136138
readonly panelId?: string;
137139
parentMenu?: MatMenuPanel | undefined;

0 commit comments

Comments
 (0)