Skip to content

Commit 1722b5a

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 1722b5a

File tree

3 files changed

+58
-6
lines changed

3 files changed

+58
-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: 44 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,23 @@ 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+
const items = Array.from(overlay.querySelectorAll('.mat-menu-panel [md-menu-item]'));
1011+
const levelOneTrigger = overlay.querySelector('.level-one-trigger')!;
1012+
1013+
dispatchMouseEvent(levelOneTrigger, 'mouseenter');
1014+
repeaterFixture.detectChanges();
1015+
expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(2, 'Expected two open menus');
1016+
}));
9991017

10001018
});
10011019

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

0 commit comments

Comments
 (0)