mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Andrew Seguin
parent
0270cf5e47
commit
7a9657040d
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user