fix(menu): unable to swap menu panel after first open (#13819)

Fixes the menu trigger being locked into the first menu panel that is passed in, after it has been opened at least once.

Relates to #13812.
This commit is contained in:
Kristiyan Kostadinov
2018-11-03 01:24:04 +01:00
committed by Jeremy Elbourn
parent 077f68e3cb
commit 1fb1c5550e
2 changed files with 89 additions and 17 deletions
+37 -17
View File
@@ -84,6 +84,7 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
private _menuOpen: boolean = false;
private _closeSubscription = Subscription.EMPTY;
private _hoverSubscription = Subscription.EMPTY;
private _menuCloseSubscription = Subscription.EMPTY;
private _scrollStrategy: () => ScrollStrategy;
// Tracking input type is necessary so it's possible to only auto-focus
@@ -95,16 +96,34 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
* @breaking-change 8.0.0
*/
@Input('mat-menu-trigger-for')
get _deprecatedMatMenuTriggerFor(): MatMenuPanel {
return this.menu;
}
get _deprecatedMatMenuTriggerFor(): MatMenuPanel { return this.menu; }
set _deprecatedMatMenuTriggerFor(v: MatMenuPanel) {
this.menu = v;
}
/** References the menu instance that the trigger is associated with. */
@Input('matMenuTriggerFor') menu: MatMenuPanel;
@Input('matMenuTriggerFor')
get menu() { return this._menu; }
set menu(menu: MatMenuPanel) {
if (menu === this._menu) {
return;
}
this._menu = menu;
this._menuCloseSubscription.unsubscribe();
if (menu) {
this._menuCloseSubscription = menu.close.asObservable().subscribe(reason => {
this._destroyMenu();
// If a click closed the menu, we should close the entire chain of nested menus.
if ((reason === 'click' || reason === 'tab') && this._parentMenu) {
this._parentMenu.closed.emit(reason);
}
});
}
}
private _menu: MatMenuPanel;
/** Data to be passed along to any lazily-rendered content. */
@Input('matMenuTriggerData') menuData: any;
@@ -151,16 +170,6 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
ngAfterContentInit() {
this._checkMenu();
this.menu.close.asObservable().subscribe(reason => {
this._destroyMenu();
// If a click closed the menu, we should close the entire chain of nested menus.
if ((reason === 'click' || reason === 'tab') && this._parentMenu) {
this._parentMenu.closed.emit(reason);
}
});
this._handleHover();
}
@@ -203,7 +212,7 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
const overlayRef = this._createOverlay();
this._setPosition(overlayRef.getConfig().positionStrategy as FlexibleConnectedPositionStrategy);
overlayRef.attach(this._portal);
overlayRef.attach(this._getPortal());
if (this.menu.lazyContent) {
this.menu.lazyContent.attach(this.menuData);
@@ -347,7 +356,6 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
*/
private _createOverlay(): OverlayRef {
if (!this._overlayRef) {
this._portal = new TemplatePortal(this.menu.templateRef, this._viewContainerRef);
const config = this._getOverlayConfig();
this._subscribeToPositions(config.positionStrategy as FlexibleConnectedPositionStrategy);
this._overlayRef = this._overlay.create(config);
@@ -531,4 +539,16 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
});
}
/** Gets the portal that should be attached to the overlay. */
private _getPortal(): TemplatePortal {
// Note that we can avoid this check by keeping the portal on the menu panel.
// While it would be cleaner, we'd have to introduce another required method on
// `MatMenuPanel`, making it harder to consume.
if (!this._portal || this._portal.templateRef !== this.menu.templateRef) {
this._portal = new TemplatePortal(this.menu.templateRef, this._viewContainerRef);
}
return this._portal;
}
}
+52
View File
@@ -517,6 +517,39 @@ describe('MatMenu', () => {
}).toThrowError(/must pass in an mat-menu instance/);
});
it('should be able to swap out a menu after the first time it is opened', fakeAsync(() => {
const fixture = createComponent(DynamicPanelMenu);
fixture.detectChanges();
expect(overlayContainerElement.textContent).toBe('');
fixture.componentInstance.trigger.openMenu();
fixture.detectChanges();
expect(overlayContainerElement.textContent).toContain('One');
expect(overlayContainerElement.textContent).not.toContain('Two');
fixture.componentInstance.trigger.closeMenu();
fixture.detectChanges();
tick(500);
fixture.detectChanges();
expect(overlayContainerElement.textContent).toBe('');
fixture.componentInstance.trigger.menu = fixture.componentInstance.secondMenu;
fixture.componentInstance.trigger.openMenu();
fixture.detectChanges();
expect(overlayContainerElement.textContent).not.toContain('One');
expect(overlayContainerElement.textContent).toContain('Two');
fixture.componentInstance.trigger.closeMenu();
fixture.detectChanges();
tick(500);
fixture.detectChanges();
expect(overlayContainerElement.textContent).toBe('');
}));
describe('lazy rendering', () => {
it('should be able to render the menu content lazily', fakeAsync(() => {
const fixture = createComponent(SimpleLazyMenu);
@@ -2048,3 +2081,22 @@ class LazyMenuWithContext {
@ViewChild('triggerTwo') triggerTwo: MatMenuTrigger;
}
@Component({
template: `
<button [matMenuTriggerFor]="one">Toggle menu</button>
<mat-menu #one="matMenu">
<button mat-menu-item>One</button>
</mat-menu>
<mat-menu #two="matMenu">
<button mat-menu-item>Two</button>
</mat-menu>
`
})
class DynamicPanelMenu {
@ViewChild(MatMenuTrigger) trigger: MatMenuTrigger;
@ViewChild('one') firstMenu: MatMenu;
@ViewChild('two') secondMenu: MatMenu;
}