@@ -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]` ,
@@ -234,9 +237,8 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
234237 }
235238
236239 ngOnDestroy ( ) {
237- if ( this . _overlayRef ) {
238- this . _overlayRef . dispose ( ) ;
239- this . _overlayRef = null ;
240+ if ( this . menu && this . _ownsMenu ( this . menu ) ) {
241+ PANELS_TO_TRIGGERS . delete ( this . menu ) ;
240242 }
241243
242244 this . _element . nativeElement . removeEventListener (
@@ -248,6 +250,11 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
248250 this . _menuCloseSubscription . unsubscribe ( ) ;
249251 this . _closingActionsSubscription . unsubscribe ( ) ;
250252 this . _hoverSubscription . unsubscribe ( ) ;
253+
254+ if ( this . _overlayRef ) {
255+ this . _overlayRef . dispose ( ) ;
256+ this . _overlayRef = null ;
257+ }
251258 }
252259
253260 /** Whether the menu is open. */
@@ -335,7 +342,6 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
335342 return ;
336343 }
337344
338- const menu = this . menu ;
339345 this . _closingActionsSubscription . unsubscribe ( ) ;
340346 this . _overlayRef . detach ( ) ;
341347
@@ -348,30 +354,10 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
348354 }
349355
350356 this . _openedBy = undefined ;
357+ this . _setIsMenuOpen ( false ) ;
351358
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 ( ) ;
359+ if ( this . menu && this . _ownsMenu ( this . menu ) ) {
360+ PANELS_TO_TRIGGERS . delete ( this . menu ) ;
375361 }
376362 }
377363
@@ -380,6 +366,15 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
380366 * the menu was opened via the keyboard.
381367 */
382368 private _initMenu ( menu : MatMenuPanel ) : void {
369+ const previousTrigger = PANELS_TO_TRIGGERS . get ( menu ) ;
370+
371+ // If the same menu is currently attached to another trigger,
372+ // we need to close it so it doesn't end up in a broken state.
373+ if ( previousTrigger && previousTrigger !== this ) {
374+ previousTrigger . closeMenu ( ) ;
375+ }
376+
377+ PANELS_TO_TRIGGERS . set ( menu , this ) ;
383378 menu . parentMenu = this . triggersSubmenu ( ) ? this . _parentMaterialMenu : undefined ;
384379 menu . direction = this . dir ;
385380 menu . focusFirstItem ( this . _openedBy || 'program' ) ;
@@ -520,10 +515,9 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
520515 const detachments = this . _overlayRef ! . detachments ( ) ;
521516 const parentClose = this . _parentMaterialMenu ? this . _parentMaterialMenu . closed : observableOf ( ) ;
522517 const hover = this . _parentMaterialMenu
523- ? this . _parentMaterialMenu . _hovered ( ) . pipe (
524- filter ( active => active !== this . _menuItemInstance ) ,
525- filter ( ( ) => this . _menuOpen ) ,
526- )
518+ ? this . _parentMaterialMenu
519+ . _hovered ( )
520+ . pipe ( filter ( active => this . _menuOpen && active !== this . _menuItemInstance ) )
527521 : observableOf ( ) ;
528522
529523 return merge ( backdrop , parentClose as Observable < MenuCloseReason > , hover , detachments ) ;
@@ -578,35 +572,14 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
578572 /** Handles the cases where the user hovers over the trigger. */
579573 private _handleHover ( ) {
580574 // 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 {
575+ if ( this . triggersSubmenu ( ) && this . _parentMaterialMenu ) {
576+ this . _hoverSubscription = this . _parentMaterialMenu . _hovered ( ) . subscribe ( active => {
577+ if ( active === this . _menuItemInstance && ! active . disabled ) {
578+ this . _openedBy = 'mouse' ;
607579 this . openMenu ( ) ;
608580 }
609581 } ) ;
582+ }
610583 }
611584
612585 /** Gets the portal that should be attached to the overlay. */
@@ -620,4 +593,13 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
620593
621594 return this . _portal ;
622595 }
596+
597+ /**
598+ * Determines whether the trigger owns a specific menu panel, at the current point in time.
599+ * This allows us to distinguish the case where the same panel is passed into multiple triggers
600+ * and multiple are open at a time.
601+ */
602+ private _ownsMenu ( menu : MatMenuPanel ) : boolean {
603+ return PANELS_TO_TRIGGERS . get ( menu ) === this ;
604+ }
623605}
0 commit comments