Skip to content

fix(menu): nested menu error when items are rendered in a repeater #6766

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 29, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -82,7 +82,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 @@ -126,7 +126,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 @@ -1029,6 +1030,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 @@ -1223,3 +1239,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'];
}