@@ -39,8 +39,8 @@ import {
3939 ViewContainerRef ,
4040} from '@angular/core' ;
4141import { normalizePassiveListenerOptions } from '@angular/cdk/platform' ;
42- import { asapScheduler , merge , Observable , of as observableOf , Subscription } from 'rxjs' ;
43- import { delay , filter , take , takeUntil } from 'rxjs/operators' ;
42+ import { merge , Observable , of as observableOf , Subscription } from 'rxjs' ;
43+ import { filter , takeUntil } from 'rxjs/operators' ;
4444import { MatMenu , MenuCloseReason } from './menu' ;
4545import { throwMatMenuRecursiveError } from './menu-errors' ;
4646import { MatMenuItem } from './menu-item' ;
@@ -81,6 +81,9 @@ const passiveEventListenerOptions = normalizePassiveListenerOptions({passive: tr
8181 */
8282export const MENU_PANEL_TOP_PADDING = 8 ;
8383
84+ /** Mapping between menu panels and the last trigger that opened them. */
85+ const PANELS_TO_TRIGGERS = new WeakMap < MatMenuPanel , MatMenuTrigger > ( ) ;
86+
8487/** Directive applied to an element that should trigger a `mat-menu`. */
8588@Directive ( {
8689 selector : `[mat-menu-trigger-for], [matMenuTriggerFor]` ,
@@ -239,6 +242,10 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
239242 this . _overlayRef = null ;
240243 }
241244
245+ if ( this . menu && this . _ownsMenu ( this . menu ) ) {
246+ PANELS_TO_TRIGGERS . delete ( this . menu ) ;
247+ }
248+
242249 this . _element . nativeElement . removeEventListener (
243250 'touchstart' ,
244251 this . _handleTouchStart ,
@@ -307,7 +314,19 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
307314
308315 /** Closes the menu. */
309316 closeMenu ( ) : void {
310- this . menu ?. close . emit ( ) ;
317+ const menu = this . menu ;
318+
319+ if ( menu ) {
320+ // If the trigger still "owns" the panel (e.g. no other trigger opened it while the current
321+ // trigger is still open), we can kick off the regular closing sequence which waits for the
322+ // animation before detaching. Otherwise we remove the overlay immediately, because its
323+ // content will soon be moved over into a new menu, leaving the panel empty.
324+ if ( this . _ownsMenu ( menu ) ) {
325+ menu . close . emit ( ) ;
326+ } else {
327+ this . _destroyMenu ( ) ;
328+ }
329+ }
311330 }
312331
313332 /**
@@ -335,7 +354,6 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
335354 return ;
336355 }
337356
338- const menu = this . menu ;
339357 this . _closingActionsSubscription . unsubscribe ( ) ;
340358 this . _overlayRef . detach ( ) ;
341359
@@ -348,30 +366,10 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
348366 }
349367
350368 this . _openedBy = undefined ;
369+ this . _setIsMenuOpen ( false ) ;
351370
352- if ( menu instanceof MatMenu ) {
353- menu . _resetAnimation ( ) ;
354-
355- if ( menu . lazyContent ) {
356- // Wait for the exit animation to finish before detaching the content.
357- menu . _animationDone
358- . pipe (
359- filter ( event => event . toState === 'void' ) ,
360- take ( 1 ) ,
361- // Interrupt if the content got re-attached.
362- takeUntil ( menu . lazyContent . _attached ) ,
363- )
364- . subscribe ( {
365- next : ( ) => menu . lazyContent ! . detach ( ) ,
366- // No matter whether the content got re-attached, reset the menu.
367- complete : ( ) => this . _setIsMenuOpen ( false ) ,
368- } ) ;
369- } else {
370- this . _setIsMenuOpen ( false ) ;
371- }
372- } else {
373- this . _setIsMenuOpen ( false ) ;
374- menu ?. lazyContent ?. detach ( ) ;
371+ if ( this . menu && this . _ownsMenu ( this . menu ) ) {
372+ PANELS_TO_TRIGGERS . delete ( this . menu ) ;
375373 }
376374 }
377375
@@ -380,6 +378,8 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
380378 * the menu was opened via the keyboard.
381379 */
382380 private _initMenu ( menu : MatMenuPanel ) : void {
381+ // The most recent trigger is considered the "owner" of the panel.
382+ PANELS_TO_TRIGGERS . set ( menu , this ) ;
383383 menu . parentMenu = this . triggersSubmenu ( ) ? this . _parentMaterialMenu : undefined ;
384384 menu . direction = this . dir ;
385385 menu . focusFirstItem ( this . _openedBy || 'program' ) ;
@@ -520,10 +520,9 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
520520 const detachments = this . _overlayRef ! . detachments ( ) ;
521521 const parentClose = this . _parentMaterialMenu ? this . _parentMaterialMenu . closed : observableOf ( ) ;
522522 const hover = this . _parentMaterialMenu
523- ? this . _parentMaterialMenu . _hovered ( ) . pipe (
524- filter ( active => active !== this . _menuItemInstance ) ,
525- filter ( ( ) => this . _menuOpen ) ,
526- )
523+ ? this . _parentMaterialMenu
524+ . _hovered ( )
525+ . pipe ( filter ( active => this . _menuOpen && active !== this . _menuItemInstance ) )
527526 : observableOf ( ) ;
528527
529528 return merge ( backdrop , parentClose as Observable < MenuCloseReason > , hover , detachments ) ;
@@ -578,35 +577,14 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
578577 /** Handles the cases where the user hovers over the trigger. */
579578 private _handleHover ( ) {
580579 // Subscribe to changes in the hovered item in order to toggle the panel.
581- if ( ! this . triggersSubmenu ( ) || ! this . _parentMaterialMenu ) {
582- return ;
583- }
584-
585- this . _hoverSubscription = this . _parentMaterialMenu
586- . _hovered ( )
587- // Since we might have multiple competing triggers for the same menu (e.g. a sub-menu
588- // with different data and triggers), we have to delay it by a tick to ensure that
589- // it won't be closed immediately after it is opened.
590- . pipe (
591- filter ( active => active === this . _menuItemInstance && ! active . disabled ) ,
592- delay ( 0 , asapScheduler ) ,
593- )
594- . subscribe ( ( ) => {
595- this . _openedBy = 'mouse' ;
596-
597- // If the same menu is used between multiple triggers, it might still be animating
598- // while the new trigger tries to re-open it. Wait for the animation to finish
599- // before doing so. Also interrupt if the user moves to another item.
600- if ( this . menu instanceof MatMenu && this . menu . _isAnimating ) {
601- // We need the `delay(0)` here in order to avoid
602- // 'changed after checked' errors in some cases. See #12194.
603- this . menu . _animationDone
604- . pipe ( take ( 1 ) , delay ( 0 , asapScheduler ) , takeUntil ( this . _parentMaterialMenu ! . _hovered ( ) ) )
605- . subscribe ( ( ) => this . openMenu ( ) ) ;
606- } else {
580+ if ( this . triggersSubmenu ( ) && this . _parentMaterialMenu ) {
581+ this . _hoverSubscription = this . _parentMaterialMenu . _hovered ( ) . subscribe ( active => {
582+ if ( active === this . _menuItemInstance && ! active . disabled ) {
583+ this . _openedBy = 'mouse' ;
607584 this . openMenu ( ) ;
608585 }
609586 } ) ;
587+ }
610588 }
611589
612590 /** Gets the portal that should be attached to the overlay. */
@@ -620,4 +598,13 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
620598
621599 return this . _portal ;
622600 }
601+
602+ /**
603+ * Determines whether the trigger owns a specific menu panel, at the current point in time.
604+ * This allows us to distinguish the case where the same panel is passed into multiple triggers
605+ * and multiple are open at a time.
606+ */
607+ private _ownsMenu ( menu : MatMenuPanel ) : boolean {
608+ return PANELS_TO_TRIGGERS . get ( menu ) === this ;
609+ }
623610}
0 commit comments