diff --git a/src/lib/menu/menu-content.ts b/src/lib/menu/menu-content.ts index d7cfa3ec271..e8d16b824ea 100644 --- a/src/lib/menu/menu-content.ts +++ b/src/lib/menu/menu-content.ts @@ -44,10 +44,10 @@ export class MatMenuContent implements OnDestroy { attach(context: any = {}) { if (!this._portal) { this._portal = new TemplatePortal(this._template, this._viewContainerRef); - } else if (this._portal.isAttached) { - this._portal.detach(); } + this.detach(); + if (!this._outlet) { this._outlet = new DomPortalOutlet(this._document.createElement('div'), this._componentFactoryResolver, this._appRef, this._injector); @@ -62,6 +62,16 @@ export class MatMenuContent implements OnDestroy { this._portal.attach(this._outlet, context); } + /** + * Detaches the content. + * @docs-private + */ + detach() { + if (this._portal.isAttached) { + this._portal.detach(); + } + } + ngOnDestroy() { if (this._outlet) { this._outlet.dispose(); diff --git a/src/lib/menu/menu-directive.ts b/src/lib/menu/menu-directive.ts index 5078d544696..83c3dbae4e6 100644 --- a/src/lib/menu/menu-directive.ts +++ b/src/lib/menu/menu-directive.ts @@ -33,6 +33,7 @@ import { OnInit, } from '@angular/core'; import {Observable} from 'rxjs/Observable'; +import {Subject} from 'rxjs/Subject'; import {merge} from 'rxjs/observable/merge'; import {Subscription} from 'rxjs/Subscription'; import {matMenuAnimations} from './menu-animations'; @@ -103,6 +104,9 @@ export class MatMenu implements OnInit, AfterContentInit, MatMenuPanel, OnDestro /** Current state of the panel animation. */ _panelAnimationState: 'void' | 'enter' = 'void'; + /** Emits whenever an animation on the menu completes. */ + _animationDone = new Subject(); + /** Parent menu of the current menu panel. */ parentMenu: MatMenuPanel | undefined; @@ -324,7 +328,7 @@ export class MatMenu implements OnInit, AfterContentInit, MatMenuPanel, OnDestro } /** Callback that is invoked when the panel animation completes. */ - _onAnimationDone(_event: AnimationEvent) { - // @deletion-target 6.0.0 Not being used anymore. To be removed. + _onAnimationDone() { + this._animationDone.next(); } } diff --git a/src/lib/menu/menu-trigger.ts b/src/lib/menu/menu-trigger.ts index f079f2497b0..ea5fc92a6ff 100644 --- a/src/lib/menu/menu-trigger.ts +++ b/src/lib/menu/menu-trigger.ts @@ -21,6 +21,7 @@ import { } from '@angular/cdk/overlay'; import {TemplatePortal} from '@angular/cdk/portal'; import {filter} from 'rxjs/operators/filter'; +import {take} from 'rxjs/operators/take'; import { AfterContentInit, Directive, @@ -238,14 +239,27 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy { /** Closes the menu and does the necessary cleanup. */ private _destroyMenu() { - if (this._overlayRef && this.menuOpen) { - this._resetMenu(); - this._closeSubscription.unsubscribe(); - this._overlayRef.detach(); + if (!this._overlayRef || !this.menuOpen) { + return; + } - if (this.menu instanceof MatMenu) { - this.menu._resetAnimation(); + const menu = this.menu; + + this._resetMenu(); + this._closeSubscription.unsubscribe(); + this._overlayRef.detach(); + + if (menu instanceof MatMenu) { + menu._resetAnimation(); + + if (menu.lazyContent) { + // Wait for the exit animation to finish before detaching the content. + menu._animationDone + .pipe(take(1)) + .subscribe(() => menu.lazyContent!.detach()); } + } else if (menu.lazyContent) { + menu.lazyContent.detach(); } } diff --git a/src/lib/menu/menu.html b/src/lib/menu/menu.html index fb226b437e3..ba48f52b654 100644 --- a/src/lib/menu/menu.html +++ b/src/lib/menu/menu.html @@ -5,6 +5,7 @@ (keydown)="_handleKeydown($event)" (click)="closed.emit('click')" [@transformMenu]="_panelAnimationState" + (@transformMenu.done)="_onAnimationDone()" tabindex="-1" role="menu">
diff --git a/src/lib/menu/menu.spec.ts b/src/lib/menu/menu.spec.ts index b1e8f3d22fa..55676f8d6fd 100644 --- a/src/lib/menu/menu.spec.ts +++ b/src/lib/menu/menu.spec.ts @@ -353,8 +353,8 @@ describe('MatMenu', () => { describe('lazy rendering', () => { it('should be able to render the menu content lazily', fakeAsync(() => { const fixture = TestBed.createComponent(SimpleLazyMenu); - fixture.detectChanges(); + fixture.detectChanges(); fixture.componentInstance.triggerEl.nativeElement.click(); fixture.detectChanges(); tick(500); @@ -366,6 +366,24 @@ describe('MatMenu', () => { expect(fixture.componentInstance.trigger.menuOpen).toBe(true, 'Expected menu to be open'); })); + it('should detach the lazy content when the menu is closed', fakeAsync(() => { + const fixture = TestBed.createComponent(SimpleLazyMenu); + + fixture.detectChanges(); + fixture.componentInstance.trigger.openMenu(); + fixture.detectChanges(); + tick(500); + + expect(fixture.componentInstance.items.length).toBeGreaterThan(0); + + fixture.componentInstance.trigger.closeMenu(); + fixture.detectChanges(); + tick(500); + fixture.detectChanges(); + + expect(fixture.componentInstance.items.length).toBe(0); + })); + it('should focus the first menu item when opening a lazy menu via keyboard', fakeAsync(() => { let zone: MockNgZone; @@ -398,8 +416,8 @@ describe('MatMenu', () => { it('should be able to open the same menu with a different context', fakeAsync(() => { const fixture = TestBed.createComponent(LazyMenuWithContext); - fixture.detectChanges(); + fixture.detectChanges(); fixture.componentInstance.triggerOne.openMenu(); fixture.detectChanges(); tick(500); @@ -1589,6 +1607,7 @@ class FakeIcon {} class SimpleLazyMenu { @ViewChild(MatMenuTrigger) trigger: MatMenuTrigger; @ViewChild('triggerEl') triggerEl: ElementRef; + @ViewChildren(MatMenuItem) items: QueryList; }