@@ -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,9 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
307314
308315 /** Closes the menu. */
309316 closeMenu ( ) : void {
310- this . menu ?. close . emit ( ) ;
317+ if ( this . _menuOpen ) {
318+ this . menu ?. close . emit ( ) ;
319+ }
311320 }
312321
313322 /**
@@ -335,7 +344,6 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
335344 return ;
336345 }
337346
338- const menu = this . menu ;
339347 this . _closingActionsSubscription . unsubscribe ( ) ;
340348 this . _overlayRef . detach ( ) ;
341349
@@ -348,30 +356,10 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
348356 }
349357
350358 this . _openedBy = undefined ;
359+ this . _setIsMenuOpen ( false ) ;
351360
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 ( ) ;
361+ if ( this . menu && this . _ownsMenu ( this . menu ) ) {
362+ PANELS_TO_TRIGGERS . delete ( this . menu ) ;
375363 }
376364 }
377365
@@ -380,6 +368,15 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
380368 * the menu was opened via the keyboard.
381369 */
382370 private _initMenu ( menu : MatMenuPanel ) : void {
371+ const previousTrigger = PANELS_TO_TRIGGERS . get ( menu ) ;
372+
373+ // If the same menu is currently attached to another trigger,
374+ // we need to close it so it doesn't end up in a broken state.
375+ if ( previousTrigger && previousTrigger !== this ) {
376+ previousTrigger . closeMenu ( ) ;
377+ }
378+
379+ PANELS_TO_TRIGGERS . set ( menu , this ) ;
383380 menu . parentMenu = this . triggersSubmenu ( ) ? this . _parentMaterialMenu : undefined ;
384381 menu . direction = this . dir ;
385382 menu . focusFirstItem ( this . _openedBy || 'program' ) ;
@@ -520,10 +517,9 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
520517 const detachments = this . _overlayRef ! . detachments ( ) ;
521518 const parentClose = this . _parentMaterialMenu ? this . _parentMaterialMenu . closed : observableOf ( ) ;
522519 const hover = this . _parentMaterialMenu
523- ? this . _parentMaterialMenu . _hovered ( ) . pipe (
524- filter ( active => active !== this . _menuItemInstance ) ,
525- filter ( ( ) => this . _menuOpen ) ,
526- )
520+ ? this . _parentMaterialMenu
521+ . _hovered ( )
522+ . pipe ( filter ( active => this . _menuOpen && active !== this . _menuItemInstance ) )
527523 : observableOf ( ) ;
528524
529525 return merge ( backdrop , parentClose as Observable < MenuCloseReason > , hover , detachments ) ;
@@ -578,35 +574,14 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
578574 /** Handles the cases where the user hovers over the trigger. */
579575 private _handleHover ( ) {
580576 // 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 {
577+ if ( this . triggersSubmenu ( ) && this . _parentMaterialMenu ) {
578+ this . _hoverSubscription = this . _parentMaterialMenu . _hovered ( ) . subscribe ( active => {
579+ if ( active === this . _menuItemInstance && ! active . disabled ) {
580+ this . _openedBy = 'mouse' ;
607581 this . openMenu ( ) ;
608582 }
609583 } ) ;
584+ }
610585 }
611586
612587 /** Gets the portal that should be attached to the overlay. */
@@ -620,4 +595,13 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
620595
621596 return this . _portal ;
622597 }
598+
599+ /**
600+ * Determines whether the trigger owns a specific menu panel, at the current point in time.
601+ * This allows us to distinguish the case where the same panel is passed into multiple triggers
602+ * and multiple are open at a time.
603+ */
604+ private _ownsMenu ( menu : MatMenuPanel ) : boolean {
605+ return PANELS_TO_TRIGGERS . get ( menu ) === this ;
606+ }
623607}
0 commit comments