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.
This commit is contained in:
Kristiyan Kostadinov
2017-09-29 22:17:44 +02:00
committed by Andrew Seguin
parent 0270cf5e47
commit 7a9657040d
3 changed files with 60 additions and 9 deletions
+15 -5
View File
@@ -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,
@@ -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';
@@ -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';
@@ -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() {
@@ -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();
}
+3 -3
View File
@@ -22,7 +22,7 @@ import {
import {TemplatePortal} from '@angular/cdk/portal';
import {filter, RxChain} from '@angular/cdk/rxjs';
import {
AfterViewInit,
AfterContentInit,
Directive,
ElementRef,
EventEmitter,
@@ -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;
@@ -125,7 +125,7 @@ export class MatMenuTrigger implements AfterViewInit, OnDestroy {
}
}
ngAfterViewInit() {
ngAfterContentInit() {
this._checkMenu();
this.menu.close.subscribe(reason => {
+42 -1
View File
@@ -48,7 +48,8 @@ describe('MatMenu', () => {
CustomMenuPanel,
CustomMenu,
NestedMenu,
NestedMenuCustomElevation
NestedMenuCustomElevation,
NestedMenuRepeater
],
providers: [
{provide: OverlayContainer, useFactory: () => {
@@ -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');
}));
});
});
@@ -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'];
}