@@ -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]` ,
@@ -239,6 +242,10 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
239
242
this . _overlayRef = null ;
240
243
}
241
244
245
+ if ( this . menu && this . _ownsMenu ( this . menu ) ) {
246
+ PANELS_TO_TRIGGERS . delete ( this . menu ) ;
247
+ }
248
+
242
249
this . _element . nativeElement . removeEventListener (
243
250
'touchstart' ,
244
251
this . _handleTouchStart ,
@@ -307,7 +314,9 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
307
314
308
315
/** Closes the menu. */
309
316
closeMenu ( ) : void {
310
- this . menu ?. close . emit ( ) ;
317
+ if ( this . _menuOpen ) {
318
+ this . menu ?. close . emit ( ) ;
319
+ }
311
320
}
312
321
313
322
/**
@@ -335,7 +344,6 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
335
344
return ;
336
345
}
337
346
338
- const menu = this . menu ;
339
347
this . _closingActionsSubscription . unsubscribe ( ) ;
340
348
this . _overlayRef . detach ( ) ;
341
349
@@ -348,30 +356,10 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
348
356
}
349
357
350
358
this . _openedBy = undefined ;
359
+ this . _setIsMenuOpen ( false ) ;
351
360
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 ) ;
375
363
}
376
364
}
377
365
@@ -380,6 +368,15 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
380
368
* the menu was opened via the keyboard.
381
369
*/
382
370
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 ) ;
383
380
menu . parentMenu = this . triggersSubmenu ( ) ? this . _parentMaterialMenu : undefined ;
384
381
menu . direction = this . dir ;
385
382
menu . focusFirstItem ( this . _openedBy || 'program' ) ;
@@ -520,10 +517,9 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
520
517
const detachments = this . _overlayRef ! . detachments ( ) ;
521
518
const parentClose = this . _parentMaterialMenu ? this . _parentMaterialMenu . closed : observableOf ( ) ;
522
519
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 ) )
527
523
: observableOf ( ) ;
528
524
529
525
return merge ( backdrop , parentClose as Observable < MenuCloseReason > , hover , detachments ) ;
@@ -578,35 +574,14 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
578
574
/** Handles the cases where the user hovers over the trigger. */
579
575
private _handleHover ( ) {
580
576
// 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' ;
607
581
this . openMenu ( ) ;
608
582
}
609
583
} ) ;
584
+ }
610
585
}
611
586
612
587
/** Gets the portal that should be attached to the overlay. */
@@ -620,4 +595,13 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
620
595
621
596
return this . _portal ;
622
597
}
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
+ }
623
607
}
0 commit comments