@@ -40,7 +40,7 @@ import {
4040} from '@angular/core' ;
4141import { normalizePassiveListenerOptions } from '@angular/cdk/platform' ;
4242import { merge , Observable , of as observableOf , Subscription } from 'rxjs' ;
43- import { filter , takeUntil } from 'rxjs/operators' ;
43+ import { filter , take , takeUntil } from 'rxjs/operators' ;
4444import { MatMenu , MenuCloseReason } from './menu' ;
4545import { throwMatMenuRecursiveError } from './menu-errors' ;
4646import { MatMenuItem } from './menu-item' ;
@@ -115,6 +115,7 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
115115 private _closingActionsSubscription = Subscription . EMPTY ;
116116 private _hoverSubscription = Subscription . EMPTY ;
117117 private _menuCloseSubscription = Subscription . EMPTY ;
118+ private _pendingRemoval : Subscription | undefined ;
118119
119120 /**
120121 * We're specifically looking for a `MatMenu` here since the generic `MatMenuPanel`
@@ -247,6 +248,7 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
247248 passiveEventListenerOptions ,
248249 ) ;
249250
251+ this . _pendingRemoval ?. unsubscribe ( ) ;
250252 this . _menuCloseSubscription . unsubscribe ( ) ;
251253 this . _closingActionsSubscription . unsubscribe ( ) ;
252254 this . _hoverSubscription . unsubscribe ( ) ;
@@ -285,24 +287,39 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
285287 return ;
286288 }
287289
290+ this . _pendingRemoval ?. unsubscribe ( ) ;
291+ const previousTrigger = PANELS_TO_TRIGGERS . get ( menu ) ;
292+ PANELS_TO_TRIGGERS . set ( menu , this ) ;
293+
294+ // If the same menu is currently attached to another trigger,
295+ // we need to close it so it doesn't end up in a broken state.
296+ if ( previousTrigger && previousTrigger !== this ) {
297+ previousTrigger . closeMenu ( ) ;
298+ }
299+
288300 const overlayRef = this . _createOverlay ( menu ) ;
289301 const overlayConfig = overlayRef . getConfig ( ) ;
290302 const positionStrategy = overlayConfig . positionStrategy as FlexibleConnectedPositionStrategy ;
291303
292304 this . _setPosition ( menu , positionStrategy ) ;
293305 overlayConfig . hasBackdrop =
294306 menu . hasBackdrop == null ? ! this . triggersSubmenu ( ) : menu . hasBackdrop ;
295- overlayRef . attach ( this . _getPortal ( menu ) ) ;
296307
297- if ( menu . lazyContent ) {
298- menu . lazyContent . attach ( this . menuData ) ;
308+ // We need the `hasAttached` check for the case where the user kicked off a removal animation,
309+ // but re-entered the menu. Re-attaching the same portal will trigger an error otherwise.
310+ if ( ! overlayRef . hasAttached ( ) ) {
311+ overlayRef . attach ( this . _getPortal ( menu ) ) ;
312+ menu . lazyContent ?. attach ( this . menuData ) ;
299313 }
300314
301315 this . _closingActionsSubscription = this . _menuClosingActions ( ) . subscribe ( ( ) => this . closeMenu ( ) ) ;
302- this . _initMenu ( menu ) ;
316+ menu . parentMenu = this . triggersSubmenu ( ) ? this . _parentMaterialMenu : undefined ;
317+ menu . direction = this . dir ;
318+ menu . focusFirstItem ( this . _openedBy || 'program' ) ;
319+ this . _setIsMenuOpen ( true ) ;
303320
304321 if ( menu instanceof MatMenu ) {
305- menu . _startAnimation ( ) ;
322+ menu . _setIsOpen ( true ) ;
306323 menu . _directDescendantItems . changes . pipe ( takeUntil ( menu . close ) ) . subscribe ( ( ) => {
307324 // Re-adjust the position without locking when the amount of items
308325 // changes so that the overlay is allowed to pick a new optimal position.
@@ -338,12 +355,28 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
338355
339356 /** Closes the menu and does the necessary cleanup. */
340357 private _destroyMenu ( reason : MenuCloseReason ) {
341- if ( ! this . _overlayRef || ! this . menuOpen ) {
358+ const overlayRef = this . _overlayRef ;
359+ const menu = this . _menu ;
360+
361+ if ( ! overlayRef || ! this . menuOpen ) {
342362 return ;
343363 }
344364
345365 this . _closingActionsSubscription . unsubscribe ( ) ;
346- this . _overlayRef . detach ( ) ;
366+ this . _pendingRemoval ?. unsubscribe ( ) ;
367+
368+ // Note that we don't wait for the animation to finish if another trigger took
369+ // over the menu, because the panel will end up empty which looks glitchy.
370+ if ( menu instanceof MatMenu && this . _ownsMenu ( menu ) ) {
371+ this . _pendingRemoval = menu . _animationDone . pipe ( take ( 1 ) ) . subscribe ( ( ) => overlayRef . detach ( ) ) ;
372+ menu . _setIsOpen ( false ) ;
373+ } else {
374+ overlayRef . detach ( ) ;
375+ }
376+
377+ if ( menu && this . _ownsMenu ( menu ) ) {
378+ PANELS_TO_TRIGGERS . delete ( menu ) ;
379+ }
347380
348381 // Always restore focus if the user is navigating using the keyboard or the menu was opened
349382 // programmatically. We don't restore for non-root triggers, because it can prevent focus
@@ -355,30 +388,6 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
355388
356389 this . _openedBy = undefined ;
357390 this . _setIsMenuOpen ( false ) ;
358-
359- if ( this . menu && this . _ownsMenu ( this . menu ) ) {
360- PANELS_TO_TRIGGERS . delete ( this . menu ) ;
361- }
362- }
363-
364- /**
365- * This method sets the menu state to open and focuses the first item if
366- * the menu was opened via the keyboard.
367- */
368- 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 ) ;
378- menu . parentMenu = this . triggersSubmenu ( ) ? this . _parentMaterialMenu : undefined ;
379- menu . direction = this . dir ;
380- menu . focusFirstItem ( this . _openedBy || 'program' ) ;
381- this . _setIsMenuOpen ( true ) ;
382391 }
383392
384393 // set state rather than toggle to support triggers sharing a menu
0 commit comments