diff --git a/src/material/menu/menu-panel.ts b/src/material/menu/menu-panel.ts index a5f0f1732a46..53b2b0094a81 100644 --- a/src/material/menu/menu-panel.ts +++ b/src/material/menu/menu-panel.ts @@ -33,6 +33,7 @@ export interface MatMenuPanel { focusFirstItem: (origin?: FocusOrigin) => void; resetActiveItem: () => void; setPositionClasses?: (x: MenuPositionX, y: MenuPositionY) => void; + openedBy?: EventEmitter; setElevation?(depth: number): void; lazyContent?: MatMenuContent; backdropClass?: string; diff --git a/src/material/menu/menu-trigger.ts b/src/material/menu/menu-trigger.ts index 75c0d1afd643..e7e356172012 100644 --- a/src/material/menu/menu-trigger.ts +++ b/src/material/menu/menu-trigger.ts @@ -35,7 +35,7 @@ import { isDevMode, } from '@angular/core'; import {normalizePassiveListenerOptions} from '@angular/cdk/platform'; -import {asapScheduler, merge, of as observableOf, Subscription} from 'rxjs'; +import {asapScheduler, merge, of as observableOf, Subscription, Subject} from 'rxjs'; import {delay, filter, take, takeUntil} from 'rxjs/operators'; import {MatMenu} from './menu'; import {throwMatMenuMissingError, throwMatMenuRecursiveError} from './menu-errors'; @@ -87,7 +87,7 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy { private _menuOpen: boolean = false; private _closingActionsSubscription = Subscription.EMPTY; private _hoverSubscription = Subscription.EMPTY; - private _menuCloseSubscription = Subscription.EMPTY; + private _menuChanged = new Subject(); private _scrollStrategy: () => ScrollStrategy; /** @@ -119,14 +119,22 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy { } this._menu = menu; - this._menuCloseSubscription.unsubscribe(); + this._menuChanged.next(); if (menu) { if (isDevMode() && menu === this._parentMenu) { throwMatMenuRecursiveError(); } - this._menuCloseSubscription = menu.close.asObservable().subscribe(reason => { + if (menu.openedBy) { + menu.openedBy.pipe(takeUntil(this._menuChanged)).subscribe((reason?: MatMenuTrigger) => { + if (reason && reason !== this) { + this._destroyMenu(); + } + }); + } + + menu.close.pipe(takeUntil(this._menuChanged)).subscribe(reason => { this._destroyMenu(); // 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 { this._element.nativeElement.removeEventListener('touchstart', this._handleTouchStart, passiveEventListenerOptions); - this._menuCloseSubscription.unsubscribe(); this._closingActionsSubscription.unsubscribe(); this._hoverSubscription.unsubscribe(); + this._menuChanged.complete(); } /** Whether the menu is open. */ @@ -324,11 +332,16 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy { * the menu was opened via the keyboard. */ private _initMenu(): void { - this.menu.parentMenu = this.triggersSubmenu() ? this._parentMenu : undefined; - this.menu.direction = this.dir; + const menu = this.menu; + menu.parentMenu = this.triggersSubmenu() ? this._parentMenu : undefined; + menu.direction = this.dir; this._setMenuElevation(); this._setIsMenuOpen(true); - this.menu.focusFirstItem(this._openedBy || 'program'); + menu.focusFirstItem(this._openedBy || 'program'); + + if (menu.openedBy) { + menu.openedBy.emit(this); + } } /** Updates the menu elevation based on the amount of parent menus that it has. */ diff --git a/src/material/menu/menu.spec.ts b/src/material/menu/menu.spec.ts index 24a5d5c5224c..b78de87f8fb0 100644 --- a/src/material/menu/menu.spec.ts +++ b/src/material/menu/menu.spec.ts @@ -905,6 +905,22 @@ describe('MatMenu', () => { expect(document.activeElement).toBe(overlayContainerElement.querySelector('.mat-menu-panel')); })); + it('should close the menu if it is opened by a different trigger', fakeAsync(() => { + const fixture = createComponent(MenuWithMultipleTriggers); + fixture.detectChanges(); + + fixture.componentInstance.triggers.first.openMenu(); + fixture.detectChanges(); + flush(); + expect(overlayContainerElement.querySelectorAll('.mat-menu-panel').length).toBe(1); + + fixture.componentInstance.triggers.last.openMenu(); + fixture.detectChanges(); + flush(); + expect(overlayContainerElement.querySelectorAll('.mat-menu-panel').length).toBe(1); + + })); + describe('lazy rendering', () => { it('should be able to render the menu content lazily', fakeAsync(() => { const fixture = createComponent(SimpleLazyMenu); @@ -2640,3 +2656,17 @@ class LazyMenuWithOnPush { }) class InvalidRecursiveMenu { } + +@Component({ + template: ` + + + + + + + ` +}) +class MenuWithMultipleTriggers { + @ViewChildren(MatMenuTrigger) triggers: QueryList; +} diff --git a/src/material/menu/menu.ts b/src/material/menu/menu.ts index 81dc31059707..943dad39dca4 100644 --- a/src/material/menu/menu.ts +++ b/src/material/menu/menu.ts @@ -245,6 +245,13 @@ export class _MatMenuBase implements AfterContentInit, MatMenuPanel @Output() readonly closed: EventEmitter = new EventEmitter(); + + /** + * Stream that emits the trigger that the menu was opened by. + * @docs-private + */ + @Output() openedBy = new EventEmitter(); + /** * Event emitted when the menu is closed. * @deprecated Switch to `closed` instead @@ -281,6 +288,7 @@ export class _MatMenuBase implements AfterContentInit, MatMenuPanel this._directDescendantItems.destroy(); this._tabSubscription.unsubscribe(); this.closed.complete(); + this.openedBy.complete(); } /** Stream that emits whenever the hovered menu item changes. */ diff --git a/tools/public_api_guard/material/menu.d.ts b/tools/public_api_guard/material/menu.d.ts index 24a5cf0c1e97..6bf34f1e8f6e 100644 --- a/tools/public_api_guard/material/menu.d.ts +++ b/tools/public_api_guard/material/menu.d.ts @@ -25,6 +25,7 @@ export declare class _MatMenuBase implements AfterContentInit, MatMenuPanel; lazyContent: MatMenuContent; + openedBy: EventEmitter; get overlapTrigger(): boolean; set overlapTrigger(value: boolean); overlayPanelClass: string | string[]; @@ -54,7 +55,7 @@ export declare class _MatMenuBase implements AfterContentInit, MatMenuPanel; + 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"]>; static ɵfac: i0.ɵɵFactoryDef<_MatMenuBase, never>; } @@ -137,6 +138,7 @@ export interface MatMenuPanel { focusFirstItem: (origin?: FocusOrigin) => void; hasBackdrop?: boolean; lazyContent?: MatMenuContent; + openedBy?: EventEmitter; overlapTrigger: boolean; overlayPanelClass?: string | string[]; readonly panelId?: string;