Skip to content

Commit bc679f8

Browse files
committed
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.
1 parent 70bd5fc commit bc679f8

File tree

3 files changed

+55
-6
lines changed

3 files changed

+55
-6
lines changed

src/lib/menu/menu-directive.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import {ESCAPE, LEFT_ARROW, RIGHT_ARROW} from '../core/keyboard/keycodes';
3535
import {merge} from 'rxjs/observable/merge';
3636
import {Observable} from 'rxjs/Observable';
3737
import {Direction} from '@angular/cdk/bidi';
38+
import {Subject} from 'rxjs/Subject';
39+
import {RxChain, switchMap, first} from '@angular/cdk/rxjs';
3840

3941
/** Default `md-menu` options that can be overridden. */
4042
export interface MdMenuDefaultOptions {
@@ -76,8 +78,11 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
7678
/** Subscription to tab events on the menu panel */
7779
private _tabSubscription: Subscription;
7880

81+
/** Stream that emits whenever the component is intialized. */
82+
private _initialized = new Subject<void>();
83+
7984
/** Config object to be passed into the menu's ngClass */
80-
_classList: any = {};
85+
_classList: {[key: string]: boolean} = {};
8186

8287
/** Current state of the panel animation. */
8388
_panelAnimationState: 'void' | 'enter-start' | 'enter' = 'void';
@@ -147,6 +152,8 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
147152
ngAfterContentInit() {
148153
this._keyManager = new FocusKeyManager<MdMenuItem>(this.items).withWrap();
149154
this._tabSubscription = this._keyManager.tabOut.subscribe(() => this.close.emit('keydown'));
155+
this._initialized.next();
156+
this._initialized.complete();
150157
}
151158

152159
ngOnDestroy() {
@@ -160,7 +167,9 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
160167

161168
/** Stream that emits whenever the hovered menu item changes. */
162169
hover(): Observable<MdMenuItem> {
163-
return merge(...this.items.map(item => item.hover));
170+
return this.items ?
171+
merge(...this.items.map(item => item.hover)) :
172+
RxChain.from(this._initialized).call(first).call(switchMap, () => this.hover()).result();
164173
}
165174

166175
/** Handle a keyboard event from the menu, delegating to the appropriate action. */

src/lib/menu/menu-trigger.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {
10-
AfterViewInit,
10+
AfterContentInit,
1111
Directive,
1212
ElementRef,
1313
EventEmitter,
@@ -85,7 +85,7 @@ export const MENU_PANEL_TOP_PADDING = 8;
8585
},
8686
exportAs: 'mdMenuTrigger'
8787
})
88-
export class MdMenuTrigger implements AfterViewInit, OnDestroy {
88+
export class MdMenuTrigger implements AfterContentInit, OnDestroy {
8989
private _portal: TemplatePortal<any>;
9090
private _overlayRef: OverlayRef | null = null;
9191
private _menuOpen: boolean = false;
@@ -149,7 +149,7 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
149149
}
150150
}
151151

152-
ngAfterViewInit() {
152+
ngAfterContentInit() {
153153
this._checkMenu();
154154

155155
this.menu.close.subscribe(reason => {

src/lib/menu/menu.spec.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ describe('MdMenu', () => {
4747
CustomMenuPanel,
4848
CustomMenu,
4949
NestedMenu,
50-
NestedMenuCustomElevation
50+
NestedMenuCustomElevation,
51+
NestedMenuRepeater
5152
],
5253
providers: [
5354
{provide: OverlayContainer, useFactory: () => {
@@ -996,6 +997,20 @@ describe('MdMenu', () => {
996997
expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(0, 'Expected no open menus');
997998
}));
998999

1000+
it('should handle the items being rendered in a repeater', fakeAsync(() => {
1001+
const repeaterFixture = TestBed.createComponent(NestedMenuRepeater);
1002+
overlay = overlayContainerElement;
1003+
1004+
expect(() => repeaterFixture.detectChanges()).not.toThrow();
1005+
1006+
repeaterFixture.componentInstance.rootTriggerEl.nativeElement.click();
1007+
repeaterFixture.detectChanges();
1008+
expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(1, 'Expected one open menu');
1009+
1010+
dispatchMouseEvent(overlay.querySelector('.level-one-trigger')!, 'mouseenter');
1011+
repeaterFixture.detectChanges();
1012+
expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(2, 'Expected two open menus');
1013+
}));
9991014

10001015
});
10011016

@@ -1177,3 +1192,28 @@ class NestedMenuCustomElevation {
11771192
@ViewChild('rootTrigger') rootTrigger: MdMenuTrigger;
11781193
@ViewChild('levelOneTrigger') levelOneTrigger: MdMenuTrigger;
11791194
}
1195+
1196+
1197+
@Component({
1198+
template: `
1199+
<button [mdMenuTriggerFor]="root" #rootTriggerEl>Toggle menu</button>
1200+
<md-menu #root="mdMenu">
1201+
<button
1202+
md-menu-item
1203+
class="level-one-trigger"
1204+
*ngFor="let item of items"
1205+
[mdMenuTriggerFor]="levelOne">{{item}}</button>
1206+
</md-menu>
1207+
1208+
<md-menu #levelOne="mdMenu">
1209+
<button md-menu-item>Four</button>
1210+
<button md-menu-item>Five</button>
1211+
</md-menu>
1212+
`
1213+
})
1214+
class NestedMenuRepeater {
1215+
@ViewChild('rootTriggerEl') rootTriggerEl: ElementRef;
1216+
@ViewChild('levelOneTrigger') levelOneTrigger: MdMenuTrigger;
1217+
1218+
items = ['one', 'two', 'three'];
1219+
}

0 commit comments

Comments
 (0)