Skip to content

Commit 7a96570

Browse files
crisbetoandrewseguin
authored andcommitted
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.
1 parent 0270cf5 commit 7a96570

File tree

3 files changed

+60
-9
lines changed

3 files changed

+60
-9
lines changed

src/lib/menu/menu-directive.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {AnimationEvent} from '@angular/animations';
1010
import {FocusKeyManager} from '@angular/cdk/a11y';
1111
import {Direction} from '@angular/cdk/bidi';
1212
import {ESCAPE, LEFT_ARROW, RIGHT_ARROW} from '@angular/cdk/keycodes';
13-
import {RxChain, startWith, switchMap} from '@angular/cdk/rxjs';
13+
import {RxChain, startWith, switchMap, first} from '@angular/cdk/rxjs';
1414
import {
1515
AfterContentInit,
1616
ChangeDetectionStrategy,
@@ -27,10 +27,12 @@ import {
2727
TemplateRef,
2828
ViewChild,
2929
ViewEncapsulation,
30+
NgZone,
3031
} from '@angular/core';
3132
import {Observable} from 'rxjs/Observable';
3233
import {merge} from 'rxjs/observable/merge';
3334
import {Subscription} from 'rxjs/Subscription';
35+
import {Subject} from 'rxjs/Subject';
3436
import {fadeInItems, transformMenu} from './menu-animations';
3537
import {throwMatMenuInvalidPositionX, throwMatMenuInvalidPositionY} from './menu-errors';
3638
import {MatMenuItem} from './menu-item';
@@ -80,7 +82,7 @@ export class MatMenu implements AfterContentInit, MatMenuPanel, OnDestroy {
8082
private _tabSubscription = Subscription.EMPTY;
8183

8284
/** Config object to be passed into the menu's ngClass */
83-
_classList: any = {};
85+
_classList: {[key: string]: boolean} = {};
8486

8587
/** Current state of the panel animation. */
8688
_panelAnimationState: 'void' | 'enter-start' | 'enter' = 'void';
@@ -145,6 +147,7 @@ export class MatMenu implements AfterContentInit, MatMenuPanel, OnDestroy {
145147

146148
constructor(
147149
private _elementRef: ElementRef,
150+
private _ngZone: NgZone,
148151
@Inject(MAT_MENU_DEFAULT_OPTIONS) private _defaultOptions: MatMenuDefaultOptions) { }
149152

150153
ngAfterContentInit() {
@@ -160,9 +163,16 @@ export class MatMenu implements AfterContentInit, MatMenuPanel, OnDestroy {
160163

161164
/** Stream that emits whenever the hovered menu item changes. */
162165
hover(): Observable<MatMenuItem> {
163-
return RxChain.from(this.items.changes)
164-
.call(startWith, this.items)
165-
.call(switchMap, (items: MatMenuItem[]) => merge(...items.map(item => item.hover)))
166+
if (this.items) {
167+
return RxChain.from(this.items.changes)
168+
.call(startWith, this.items)
169+
.call(switchMap, (items: MatMenuItem[]) => merge(...items.map(item => item.hover)))
170+
.result();
171+
}
172+
173+
return RxChain.from(this._ngZone.onStable.asObservable())
174+
.call(first)
175+
.call(switchMap, () => this.hover())
166176
.result();
167177
}
168178

src/lib/menu/menu-trigger.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
import {TemplatePortal} from '@angular/cdk/portal';
2323
import {filter, RxChain} from '@angular/cdk/rxjs';
2424
import {
25-
AfterViewInit,
25+
AfterContentInit,
2626
Directive,
2727
ElementRef,
2828
EventEmitter,
@@ -81,7 +81,7 @@ export const MENU_PANEL_TOP_PADDING = 8;
8181
},
8282
exportAs: 'matMenuTrigger'
8383
})
84-
export class MatMenuTrigger implements AfterViewInit, OnDestroy {
84+
export class MatMenuTrigger implements AfterContentInit, OnDestroy {
8585
private _portal: TemplatePortal<any>;
8686
private _overlayRef: OverlayRef | null = null;
8787
private _menuOpen: boolean = false;
@@ -125,7 +125,7 @@ export class MatMenuTrigger implements AfterViewInit, OnDestroy {
125125
}
126126
}
127127

128-
ngAfterViewInit() {
128+
ngAfterContentInit() {
129129
this._checkMenu();
130130

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

src/lib/menu/menu.spec.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ describe('MatMenu', () => {
4848
CustomMenuPanel,
4949
CustomMenu,
5050
NestedMenu,
51-
NestedMenuCustomElevation
51+
NestedMenuCustomElevation,
52+
NestedMenuRepeater
5253
],
5354
providers: [
5455
{provide: OverlayContainer, useFactory: () => {
@@ -1046,6 +1047,21 @@ describe('MatMenu', () => {
10461047
expect(event.preventDefault).toHaveBeenCalled();
10471048
});
10481049

1050+
it('should handle the items being rendered in a repeater', fakeAsync(() => {
1051+
const repeaterFixture = TestBed.createComponent(NestedMenuRepeater);
1052+
overlay = overlayContainerElement;
1053+
1054+
expect(() => repeaterFixture.detectChanges()).not.toThrow();
1055+
1056+
repeaterFixture.componentInstance.rootTriggerEl.nativeElement.click();
1057+
repeaterFixture.detectChanges();
1058+
expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(1, 'Expected one open menu');
1059+
1060+
dispatchMouseEvent(overlay.querySelector('.level-one-trigger')!, 'mouseenter');
1061+
repeaterFixture.detectChanges();
1062+
expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(2, 'Expected two open menus');
1063+
}));
1064+
10491065
});
10501066

10511067
});
@@ -1243,3 +1259,28 @@ class NestedMenuCustomElevation {
12431259
@ViewChild('rootTrigger') rootTrigger: MatMenuTrigger;
12441260
@ViewChild('levelOneTrigger') levelOneTrigger: MatMenuTrigger;
12451261
}
1262+
1263+
1264+
@Component({
1265+
template: `
1266+
<button [matMenuTriggerFor]="root" #rootTriggerEl>Toggle menu</button>
1267+
<mat-menu #root="matMenu">
1268+
<button
1269+
mat-menu-item
1270+
class="level-one-trigger"
1271+
*ngFor="let item of items"
1272+
[matMenuTriggerFor]="levelOne">{{item}}</button>
1273+
</mat-menu>
1274+
1275+
<mat-menu #levelOne="matMenu">
1276+
<button mat-menu-item>Four</button>
1277+
<button mat-menu-item>Five</button>
1278+
</mat-menu>
1279+
`
1280+
})
1281+
class NestedMenuRepeater {
1282+
@ViewChild('rootTriggerEl') rootTriggerEl: ElementRef;
1283+
@ViewChild('levelOneTrigger') levelOneTrigger: MatMenuTrigger;
1284+
1285+
items = ['one', 'two', 'three'];
1286+
}

0 commit comments

Comments
 (0)