@@ -39,8 +39,8 @@ import {
39
39
ViewContainerRef ,
40
40
} from '@angular/core' ;
41
41
import { 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' ;
44
44
import { MatMenu , MenuCloseReason } from './menu' ;
45
45
import { throwMatMenuRecursiveError } from './menu-errors' ;
46
46
import { MatMenuItem } from './menu-item' ;
@@ -81,6 +81,9 @@ const passiveEventListenerOptions = normalizePassiveListenerOptions({passive: tr
81
81
*/
82
82
export const MENU_PANEL_TOP_PADDING = 8 ;
83
83
84
+ /** Mapping between menu panels and the last trigger that opened them. */
85
+ const PANELS_TO_TRIGGERS = new WeakMap < MatMenuPanel , MatMenuTrigger > ( ) ;
86
+
84
87
/** Directive applied to an element that should trigger a `mat-menu`. */
85
88
@Directive ( {
86
89
selector : `[mat-menu-trigger-for], [matMenuTriggerFor]` ,
@@ -234,9 +237,8 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
234
237
}
235
238
236
239
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 ) ;
240
242
}
241
243
242
244
this . _element . nativeElement . removeEventListener (
@@ -248,6 +250,11 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
248
250
this . _menuCloseSubscription . unsubscribe ( ) ;
249
251
this . _closingActionsSubscription . unsubscribe ( ) ;
250
252
this . _hoverSubscription . unsubscribe ( ) ;
253
+
254
+ if ( this . _overlayRef ) {
255
+ this . _overlayRef . dispose ( ) ;
256
+ this . _overlayRef = null ;
257
+ }
251
258
}
252
259
253
260
/** Whether the menu is open. */
@@ -335,7 +342,6 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
335
342
return ;
336
343
}
337
344
338
- const menu = this . menu ;
339
345
this . _closingActionsSubscription . unsubscribe ( ) ;
340
346
this . _overlayRef . detach ( ) ;
341
347
@@ -348,30 +354,10 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
348
354
}
349
355
350
356
this . _openedBy = undefined ;
357
+ this . _setIsMenuOpen ( false ) ;
351
358
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 ) ;
375
361
}
376
362
}
377
363
@@ -380,6 +366,15 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
380
366
* the menu was opened via the keyboard.
381
367
*/
382
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 ) ;
383
378
menu . parentMenu = this . triggersSubmenu ( ) ? this . _parentMaterialMenu : undefined ;
384
379
menu . direction = this . dir ;
385
380
menu . focusFirstItem ( this . _openedBy || 'program' ) ;
@@ -520,10 +515,9 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
520
515
const detachments = this . _overlayRef ! . detachments ( ) ;
521
516
const parentClose = this . _parentMaterialMenu ? this . _parentMaterialMenu . closed : observableOf ( ) ;
522
517
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 ) )
527
521
: observableOf ( ) ;
528
522
529
523
return merge ( backdrop , parentClose as Observable < MenuCloseReason > , hover , detachments ) ;
@@ -578,35 +572,14 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
578
572
/** Handles the cases where the user hovers over the trigger. */
579
573
private _handleHover ( ) {
580
574
// 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' ;
607
579
this . openMenu ( ) ;
608
580
}
609
581
} ) ;
582
+ }
610
583
}
611
584
612
585
/** Gets the portal that should be attached to the overlay. */
@@ -620,4 +593,13 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
620
593
621
594
return this . _portal ;
622
595
}
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
+ }
623
605
}
0 commit comments