Skip to content

Commit

Permalink
fix(menu): nested menu error when items are rendered in a repeater (#…
Browse files Browse the repository at this point in the history
…6766)

Fixes an error that was being thrown when the menu items that trigger a sub-menu are rendered in a repeater.

Fixes #6765.
  • Loading branch information
crisbeto authored and andrewseguin committed Sep 29, 2017
1 parent 0270cf5 commit 7a96570
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 9 deletions.
20 changes: 15 additions & 5 deletions src/lib/menu/menu-directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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() {
Expand All @@ -160,9 +163,16 @@ export class MatMenu implements AfterContentInit, MatMenuPanel, OnDestroy {

/** Stream that emits whenever the hovered menu item changes. */
hover(): Observable<MatMenuItem> {
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();
}

Expand Down
6 changes: 3 additions & 3 deletions src/lib/menu/menu-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
import {TemplatePortal} from '@angular/cdk/portal';
import {filter, RxChain} from '@angular/cdk/rxjs';
import {
AfterViewInit,
AfterContentInit,
Directive,
ElementRef,
EventEmitter,
Expand Down Expand Up @@ -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<any>;
private _overlayRef: OverlayRef | null = null;
private _menuOpen: boolean = false;
Expand Down Expand Up @@ -125,7 +125,7 @@ export class MatMenuTrigger implements AfterViewInit, OnDestroy {
}
}

ngAfterViewInit() {
ngAfterContentInit() {
this._checkMenu();

this.menu.close.subscribe(reason => {
Expand Down
43 changes: 42 additions & 1 deletion src/lib/menu/menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ describe('MatMenu', () => {
CustomMenuPanel,
CustomMenu,
NestedMenu,
NestedMenuCustomElevation
NestedMenuCustomElevation,
NestedMenuRepeater
],
providers: [
{provide: OverlayContainer, useFactory: () => {
Expand Down Expand Up @@ -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');
}));

});

});
Expand Down Expand Up @@ -1243,3 +1259,28 @@ class NestedMenuCustomElevation {
@ViewChild('rootTrigger') rootTrigger: MatMenuTrigger;
@ViewChild('levelOneTrigger') levelOneTrigger: MatMenuTrigger;
}


@Component({
template: `
<button [matMenuTriggerFor]="root" #rootTriggerEl>Toggle menu</button>
<mat-menu #root="matMenu">
<button
mat-menu-item
class="level-one-trigger"
*ngFor="let item of items"
[matMenuTriggerFor]="levelOne">{{item}}</button>
</mat-menu>
<mat-menu #levelOne="matMenu">
<button mat-menu-item>Four</button>
<button mat-menu-item>Five</button>
</mat-menu>
`
})
class NestedMenuRepeater {
@ViewChild('rootTriggerEl') rootTriggerEl: ElementRef;
@ViewChild('levelOneTrigger') levelOneTrigger: MatMenuTrigger;

items = ['one', 'two', 'three'];
}

0 comments on commit 7a96570

Please sign in to comment.