From 1722b5a7dd58768dbf5cde713fff5fc35da9c55c Mon Sep 17 00:00:00 2001 From: crisbeto Date: Thu, 31 Aug 2017 20:08:03 +0200 Subject: [PATCH] fix(menu): nested menu error when items are rendered in a repeater Fixes an error that was being thrown when the menu items that trigger a sub-menu are rendered in a repeater. Fixes #6765. --- src/lib/menu/menu-directive.ts | 13 ++++++++-- src/lib/menu/menu-trigger.ts | 6 ++--- src/lib/menu/menu.spec.ts | 45 +++++++++++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/lib/menu/menu-directive.ts b/src/lib/menu/menu-directive.ts index a0344ba141ce..b5b40e7e2670 100644 --- a/src/lib/menu/menu-directive.ts +++ b/src/lib/menu/menu-directive.ts @@ -35,6 +35,8 @@ import {ESCAPE, LEFT_ARROW, RIGHT_ARROW} from '../core/keyboard/keycodes'; import {merge} from 'rxjs/observable/merge'; import {Observable} from 'rxjs/Observable'; import {Direction} from '@angular/cdk/bidi'; +import {Subject} from 'rxjs/Subject'; +import {RxChain, switchMap, first} from '@angular/cdk/rxjs'; /** Default `md-menu` options that can be overridden. */ export interface MdMenuDefaultOptions { @@ -76,8 +78,11 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy { /** Subscription to tab events on the menu panel */ private _tabSubscription: Subscription; + /** Stream that emits whenever the component is intialized. */ + private _initialized = new Subject(); + /** Config object to be passed into the menu's ngClass */ - _classList: any = {}; + _classList: {[key: string]: boolean} = {}; /** Current state of the panel animation. */ _panelAnimationState: 'void' | 'enter-start' | 'enter' = 'void'; @@ -147,6 +152,8 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy { ngAfterContentInit() { this._keyManager = new FocusKeyManager(this.items).withWrap(); this._tabSubscription = this._keyManager.tabOut.subscribe(() => this.close.emit('keydown')); + this._initialized.next(); + this._initialized.complete(); } ngOnDestroy() { @@ -160,7 +167,9 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy { /** Stream that emits whenever the hovered menu item changes. */ hover(): Observable { - return merge(...this.items.map(item => item.hover)); + return this.items ? + merge(...this.items.map(item => item.hover)) : + RxChain.from(this._initialized).call(first).call(switchMap, () => this.hover()).result(); } /** Handle a keyboard event from the menu, delegating to the appropriate action. */ diff --git a/src/lib/menu/menu-trigger.ts b/src/lib/menu/menu-trigger.ts index b2bda1683e12..94bb6469ee81 100644 --- a/src/lib/menu/menu-trigger.ts +++ b/src/lib/menu/menu-trigger.ts @@ -7,7 +7,7 @@ */ import { - AfterViewInit, + AfterContentInit, Directive, ElementRef, EventEmitter, @@ -85,7 +85,7 @@ export const MENU_PANEL_TOP_PADDING = 8; }, exportAs: 'mdMenuTrigger' }) -export class MdMenuTrigger implements AfterViewInit, OnDestroy { +export class MdMenuTrigger implements AfterContentInit, OnDestroy { private _portal: TemplatePortal; private _overlayRef: OverlayRef | null = null; private _menuOpen: boolean = false; @@ -149,7 +149,7 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy { } } - ngAfterViewInit() { + ngAfterContentInit() { this._checkMenu(); this.menu.close.subscribe(reason => { diff --git a/src/lib/menu/menu.spec.ts b/src/lib/menu/menu.spec.ts index 7b77a6940f44..b1cc1fd9b19a 100644 --- a/src/lib/menu/menu.spec.ts +++ b/src/lib/menu/menu.spec.ts @@ -47,7 +47,8 @@ describe('MdMenu', () => { CustomMenuPanel, CustomMenu, NestedMenu, - NestedMenuCustomElevation + NestedMenuCustomElevation, + NestedMenuRepeater ], providers: [ {provide: OverlayContainer, useFactory: () => { @@ -996,6 +997,23 @@ describe('MdMenu', () => { expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(0, 'Expected no open menus'); })); + it('should handle the items being rendered in a repeater', fakeAsync(() => { + const repeaterFixture = TestBed.createComponent(NestedMenuRepeater); + overlay = overlayContainerElement; + + expect(() => repeaterFixture.detectChanges()).not.toThrow(); + + repeaterFixture.componentInstance.rootTriggerEl.nativeElement.click(); + repeaterFixture.detectChanges(); + expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(1, 'Expected one open menu'); + + const items = Array.from(overlay.querySelectorAll('.mat-menu-panel [md-menu-item]')); + const levelOneTrigger = overlay.querySelector('.level-one-trigger')!; + + dispatchMouseEvent(levelOneTrigger, 'mouseenter'); + repeaterFixture.detectChanges(); + expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(2, 'Expected two open menus'); + })); }); @@ -1177,3 +1195,28 @@ class NestedMenuCustomElevation { @ViewChild('rootTrigger') rootTrigger: MdMenuTrigger; @ViewChild('levelOneTrigger') levelOneTrigger: MdMenuTrigger; } + + +@Component({ + template: ` + + + + + + + + + + ` +}) +class NestedMenuRepeater { + @ViewChild('rootTriggerEl') rootTriggerEl: ElementRef; + @ViewChild('levelOneTrigger') levelOneTrigger: MdMenuTrigger; + + items = ['one', 'two', 'three']; +}