Skip to content

Commit a431705

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 89b5fa8 commit a431705

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
@@ -35,7 +35,7 @@ import {
3535
isDevMode,
3636
} from '@angular/core';
3737
import {normalizePassiveListenerOptions} from '@angular/cdk/platform';
38-
import {asapScheduler, merge, of as observableOf, Subscription} from 'rxjs';
38+
import {asapScheduler, merge, of as observableOf, Subscription, Subject} from 'rxjs';
3939
import {delay, filter, take, takeUntil} from 'rxjs/operators';
4040
import {MatMenu} from './menu';
4141
import {throwMatMenuMissingError, throwMatMenuRecursiveError} 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,14 +119,22 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
119119
}
120120

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

124124
if (menu) {
125125
if (isDevMode() && menu === this._parentMenu) {
126126
throwMatMenuRecursiveError();
127127
}
128128

129-
this._menuCloseSubscription = menu.close.asObservable().subscribe(reason => {
129+
if (menu.openedBy) {
130+
menu.openedBy.pipe(takeUntil(this._menuChanged)).subscribe((reason?: MatMenuTrigger) => {
131+
if (reason && reason !== this) {
132+
this._destroyMenu();
133+
}
134+
});
135+
}
136+
137+
menu.close.pipe(takeUntil(this._menuChanged)).subscribe(reason => {
130138
this._destroyMenu();
131139

132140
// If a click closed the menu, we should close the entire chain of nested menus.
@@ -209,9 +217,9 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
209217
this._element.nativeElement.removeEventListener('touchstart', this._handleTouchStart,
210218
passiveEventListenerOptions);
211219

212-
this._menuCloseSubscription.unsubscribe();
213220
this._closingActionsSubscription.unsubscribe();
214221
this._hoverSubscription.unsubscribe();
222+
this._menuChanged.complete();
215223
}
216224

217225
/** Whether the menu is open. */
@@ -324,11 +332,16 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
324332
* the menu was opened via the keyboard.
325333
*/
326334
private _initMenu(): void {
327-
this.menu.parentMenu = this.triggersSubmenu() ? this._parentMenu : undefined;
328-
this.menu.direction = this.dir;
335+
const menu = this.menu;
336+
menu.parentMenu = this.triggersSubmenu() ? this._parentMenu : undefined;
337+
menu.direction = this.dir;
329338
this._setMenuElevation();
330339
this._setIsMenuOpen(true);
331-
this.menu.focusFirstItem(this._openedBy || 'program');
340+
menu.focusFirstItem(this._openedBy || 'program');
341+
342+
if (menu.openedBy) {
343+
menu.openedBy.emit(this);
344+
}
332345
}
333346

334347
/** 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
@@ -905,6 +905,22 @@ describe('MatMenu', () => {
905905
expect(document.activeElement).toBe(overlayContainerElement.querySelector('.mat-menu-panel'));
906906
}));
907907

908+
it('should close the menu if it is opened by a different trigger', fakeAsync(() => {
909+
const fixture = createComponent(MenuWithMultipleTriggers);
910+
fixture.detectChanges();
911+
912+
fixture.componentInstance.triggers.first.openMenu();
913+
fixture.detectChanges();
914+
flush();
915+
expect(overlayContainerElement.querySelectorAll('.mat-menu-panel').length).toBe(1);
916+
917+
fixture.componentInstance.triggers.last.openMenu();
918+
fixture.detectChanges();
919+
flush();
920+
expect(overlayContainerElement.querySelectorAll('.mat-menu-panel').length).toBe(1);
921+
922+
}));
923+
908924
describe('lazy rendering', () => {
909925
it('should be able to render the menu content lazily', fakeAsync(() => {
910926
const fixture = createComponent(SimpleLazyMenu);
@@ -2640,3 +2656,17 @@ class LazyMenuWithOnPush {
26402656
})
26412657
class InvalidRecursiveMenu {
26422658
}
2659+
2660+
@Component({
2661+
template: `
2662+
<button [matMenuTriggerFor]="menu">First trigger</button>
2663+
<button [matMenuTriggerFor]="menu">Second trigger</button>
2664+
<mat-menu #menu="matMenu">
2665+
<button mat-menu-item>Item one</button>
2666+
<button mat-menu-item>Item two</button>
2667+
</mat-menu>
2668+
`
2669+
})
2670+
class MenuWithMultipleTriggers {
2671+
@ViewChildren(MatMenuTrigger) triggers: QueryList<MatMenuTrigger>;
2672+
}

src/material/menu/menu.ts

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

248+
249+
/**
250+
* Stream that emits the trigger that the menu was opened by.
251+
* @docs-private
252+
*/
253+
@Output() openedBy = new EventEmitter<any>();
254+
248255
/**
249256
* Event emitted when the menu is closed.
250257
* @deprecated Switch to `closed` instead
@@ -281,6 +288,7 @@ export class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatMenuItem>
281288
this._directDescendantItems.destroy();
282289
this._tabSubscription.unsubscribe();
283290
this.closed.complete();
291+
this.openedBy.complete();
284292
}
285293

286294
/** 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
overlayPanelClass: string | string[];
@@ -54,7 +55,7 @@ export declare class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatM
5455
setPositionClasses(posX?: MenuPositionX, posY?: MenuPositionY): void;
5556
static ngAcceptInputType_hasBackdrop: BooleanInput;
5657
static ngAcceptInputType_overlapTrigger: BooleanInput;
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"; "close": "close"; }, ["lazyContent", "_allItems", "items"]>;
58+
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"]>;
5859
static ɵfac: i0.ɵɵFactoryDef<_MatMenuBase, never>;
5960
}
6061

@@ -137,6 +138,7 @@ export interface MatMenuPanel<T = any> {
137138
focusFirstItem: (origin?: FocusOrigin) => void;
138139
hasBackdrop?: boolean;
139140
lazyContent?: MatMenuContent;
141+
openedBy?: EventEmitter<any>;
140142
overlapTrigger: boolean;
141143
overlayPanelClass?: string | string[];
142144
readonly panelId?: string;

0 commit comments

Comments
 (0)