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
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 committed Aug 31, 2017
1 parent 70bd5fc commit 1722b5a
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 6 deletions.
13 changes: 11 additions & 2 deletions src/lib/menu/menu-directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import {ESCAPE, LEFT_ARROW, RIGHT_ARROW} from '../core/keyboard/keycodes';
import {merge} from 'rxjs/observable/merge';
import {Observable} from 'rxjs/Observable';
import {Direction} from '@angular/cdk/bidi';
import {Subject} from 'rxjs/Subject';
import {RxChain, switchMap, first} from '@angular/cdk/rxjs';

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

/** Stream that emits whenever the component is intialized. */
private _initialized = new Subject<void>();

/** 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 @@ -147,6 +152,8 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
ngAfterContentInit() {
this._keyManager = new FocusKeyManager<MdMenuItem>(this.items).withWrap();
this._tabSubscription = this._keyManager.tabOut.subscribe(() => this.close.emit('keydown'));
this._initialized.next();
this._initialized.complete();
}

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

/** Stream that emits whenever the hovered menu item changes. */
hover(): Observable<MdMenuItem> {
return merge(...this.items.map(item => item.hover));
return this.items ?
merge(...this.items.map(item => item.hover)) :
RxChain.from(this._initialized).call(first).call(switchMap, () => this.hover()).result();
}

/** Handle a keyboard event from the menu, delegating to the appropriate action. */
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 @@ -7,7 +7,7 @@
*/

import {
AfterViewInit,
AfterContentInit,
Directive,
ElementRef,
EventEmitter,
Expand Down Expand Up @@ -85,7 +85,7 @@ export const MENU_PANEL_TOP_PADDING = 8;
},
exportAs: 'mdMenuTrigger'
})
export class MdMenuTrigger implements AfterViewInit, OnDestroy {
export class MdMenuTrigger implements AfterContentInit, OnDestroy {
private _portal: TemplatePortal<any>;
private _overlayRef: OverlayRef | null = null;
private _menuOpen: boolean = false;
Expand Down Expand Up @@ -149,7 +149,7 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
}
}

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

this.menu.close.subscribe(reason => {
Expand Down
45 changes: 44 additions & 1 deletion src/lib/menu/menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ describe('MdMenu', () => {
CustomMenuPanel,
CustomMenu,
NestedMenu,
NestedMenuCustomElevation
NestedMenuCustomElevation,
NestedMenuRepeater
],
providers: [
{provide: OverlayContainer, useFactory: () => {
Expand Down Expand Up @@ -996,6 +997,23 @@ describe('MdMenu', () => {
expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(0, 'Expected no open menus');
}));

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');

const items = Array.from(overlay.querySelectorAll('.mat-menu-panel [md-menu-item]'));
const levelOneTrigger = overlay.querySelector('.level-one-trigger')!;

dispatchMouseEvent(levelOneTrigger, 'mouseenter');
repeaterFixture.detectChanges();
expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(2, 'Expected two open menus');
}));

});

Expand Down Expand Up @@ -1177,3 +1195,28 @@ class NestedMenuCustomElevation {
@ViewChild('rootTrigger') rootTrigger: MdMenuTrigger;
@ViewChild('levelOneTrigger') levelOneTrigger: MdMenuTrigger;
}


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

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

0 comments on commit 1722b5a

Please sign in to comment.