From 7a9657040d9a75378ae5cb3f70e7c0c23c762b7f Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 29 Sep 2017 22:17:44 +0200 Subject: [PATCH] fix(menu): nested menu error when items are rendered in a repeater (#6766) 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 | 20 ++++++++++++---- src/lib/menu/menu-trigger.ts | 6 ++--- src/lib/menu/menu.spec.ts | 43 +++++++++++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/lib/menu/menu-directive.ts b/src/lib/menu/menu-directive.ts index 81243ac07b97..aa14055003b7 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 {throwMatMenuInvalidPositionX, throwMatMenuInvalidPositionY} from './menu-errors'; import {MatMenuItem} from './menu-item'; @@ -80,7 +82,7 @@ export class MatMenu implements AfterContentInit, MatMenuPanel, 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 MatMenu implements AfterContentInit, MatMenuPanel, OnDestroy { constructor( private _elementRef: ElementRef, + private _ngZone: NgZone, @Inject(MAT_MENU_DEFAULT_OPTIONS) private _defaultOptions: MatMenuDefaultOptions) { } ngAfterContentInit() { @@ -160,9 +163,16 @@ export class MatMenu implements AfterContentInit, MatMenuPanel, 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: MatMenuItem[]) => merge(...items.map(item => item.hover))) + if (this.items) { + return RxChain.from(this.items.changes) + .call(startWith, this.items) + .call(switchMap, (items: MatMenuItem[]) => 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 81808968e894..a37c541f3e1e 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, @@ -81,7 +81,7 @@ export const MENU_PANEL_TOP_PADDING = 8; }, exportAs: 'matMenuTrigger' }) -export class MatMenuTrigger implements AfterViewInit, OnDestroy { +export class MatMenuTrigger implements AfterContentInit, OnDestroy { private _portal: TemplatePortal; private _overlayRef: OverlayRef | null = null; private _menuOpen: boolean = false; @@ -125,7 +125,7 @@ export class MatMenuTrigger 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 0370cd13d8fb..83b8cb8db4e7 100644 --- a/src/lib/menu/menu.spec.ts +++ b/src/lib/menu/menu.spec.ts @@ -48,7 +48,8 @@ describe('MatMenu', () => { CustomMenuPanel, CustomMenu, NestedMenu, - NestedMenuCustomElevation + NestedMenuCustomElevation, + NestedMenuRepeater ], providers: [ {provide: OverlayContainer, useFactory: () => { @@ -1046,6 +1047,21 @@ describe('MatMenu', () => { 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'); + })); + }); }); @@ -1243,3 +1259,28 @@ class NestedMenuCustomElevation { @ViewChild('rootTrigger') rootTrigger: MatMenuTrigger; @ViewChild('levelOneTrigger') levelOneTrigger: MatMenuTrigger; } + + +@Component({ + template: ` + + + + + + + + + + ` +}) +class NestedMenuRepeater { + @ViewChild('rootTriggerEl') rootTriggerEl: ElementRef; + @ViewChild('levelOneTrigger') levelOneTrigger: MatMenuTrigger; + + items = ['one', 'two', 'three']; +}