diff --git a/src/lib/menu/menu-directive.ts b/src/lib/menu/menu-directive.ts index aa7d1c8dfef1..9c9efa648f51 100644 --- a/src/lib/menu/menu-directive.ts +++ b/src/lib/menu/menu-directive.ts @@ -10,7 +10,7 @@ import {AnimationEvent} from '@angular/animations'; import {FocusKeyManager} from '@angular/cdk/a11y'; import {Direction} from '@angular/cdk/bidi'; import {ESCAPE, LEFT_ARROW, RIGHT_ARROW} from '@angular/cdk/keycodes'; -import {RxChain, startWith, switchMap} from '@angular/cdk/rxjs'; +import {RxChain, startWith, switchMap, first} from '@angular/cdk/rxjs'; import { AfterContentInit, ChangeDetectionStrategy, @@ -27,10 +27,12 @@ import { TemplateRef, ViewChild, ViewEncapsulation, + NgZone, } from '@angular/core'; import {Observable} from 'rxjs/Observable'; import {merge} from 'rxjs/observable/merge'; import {Subscription} from 'rxjs/Subscription'; +import {Subject} from 'rxjs/Subject'; import {fadeInItems, transformMenu} from './menu-animations'; import {throwMdMenuInvalidPositionX, throwMdMenuInvalidPositionY} from './menu-errors'; import {MdMenuItem} from './menu-item'; @@ -80,7 +82,7 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy { private _tabSubscription = Subscription.EMPTY; /** 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'; @@ -145,6 +147,7 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy { constructor( private _elementRef: ElementRef, + private _ngZone: NgZone, @Inject(MD_MENU_DEFAULT_OPTIONS) private _defaultOptions: MdMenuDefaultOptions) { } ngAfterContentInit() { @@ -160,9 +163,16 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy { /** Stream that emits whenever the hovered menu item changes. */ hover(): Observable { - return RxChain.from(this.items.changes) - .call(startWith, this.items) - .call(switchMap, (items: MdMenuItem[]) => merge(...items.map(item => item.hover))) + if (this.items) { + return RxChain.from(this.items.changes) + .call(startWith, this.items) + .call(switchMap, (items: MdMenuItem[]) => merge(...items.map(item => item.hover))) + .result(); + } + + return RxChain.from(this._ngZone.onStable.asObservable()) + .call(first) + .call(switchMap, () => this.hover()) .result(); } diff --git a/src/lib/menu/menu-trigger.ts b/src/lib/menu/menu-trigger.ts index 09d2e9c63764..45658048b2f0 100644 --- a/src/lib/menu/menu-trigger.ts +++ b/src/lib/menu/menu-trigger.ts @@ -22,7 +22,7 @@ import { import {TemplatePortal} from '@angular/cdk/portal'; import {filter, RxChain} from '@angular/cdk/rxjs'; import { - AfterViewInit, + AfterContentInit, Directive, ElementRef, EventEmitter, @@ -83,7 +83,7 @@ export const MENU_PANEL_TOP_PADDING = 8; }, exportAs: 'mdMenuTrigger, matMenuTrigger' }) -export class MdMenuTrigger implements AfterViewInit, OnDestroy { +export class MdMenuTrigger implements AfterContentInit, OnDestroy { private _portal: TemplatePortal; private _overlayRef: OverlayRef | null = null; private _menuOpen: boolean = false; @@ -147,7 +147,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 8b07cf95975c..7c3487d0deef 100644 --- a/src/lib/menu/menu.spec.ts +++ b/src/lib/menu/menu.spec.ts @@ -48,7 +48,8 @@ describe('MdMenu', () => { CustomMenuPanel, CustomMenu, NestedMenu, - NestedMenuCustomElevation + NestedMenuCustomElevation, + NestedMenuRepeater ], providers: [ {provide: OverlayContainer, useFactory: () => { @@ -1029,6 +1030,21 @@ describe('MdMenu', () => { expect(event.preventDefault).toHaveBeenCalled(); }); + 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'); + + dispatchMouseEvent(overlay.querySelector('.level-one-trigger')!, 'mouseenter'); + repeaterFixture.detectChanges(); + expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(2, 'Expected two open menus'); + })); + }); }); @@ -1223,3 +1239,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']; +}