fix(menu): mark lazy menu content as dirty before attach (#16506)

This fixes a Google presubmit issue where the menu's `@ContentChildren`
aren't updated after attaching lazy `MatMenuContent`.
This commit is contained in:
Jeremy Elbourn
2019-07-16 16:51:51 -07:00
committed by GitHub
parent 74794d4fa7
commit 5fb0dc66a1
3 changed files with 94 additions and 39 deletions
+25 -12
View File
@@ -6,18 +6,19 @@
* found in the LICENSE file at https://angular.io/license
*/
import {
Directive,
TemplateRef,
ComponentFactoryResolver,
ApplicationRef,
Injector,
ViewContainerRef,
Inject,
OnDestroy,
} from '@angular/core';
import {TemplatePortal, DomPortalOutlet} from '@angular/cdk/portal';
import {DomPortalOutlet, TemplatePortal} from '@angular/cdk/portal';
import {DOCUMENT} from '@angular/common';
import {
ApplicationRef,
ChangeDetectorRef,
ComponentFactoryResolver,
Directive,
Inject,
Injector,
OnDestroy,
TemplateRef,
ViewContainerRef,
} from '@angular/core';
import {Subject} from 'rxjs';
/**
@@ -39,7 +40,8 @@ export class MatMenuContent implements OnDestroy {
private _appRef: ApplicationRef,
private _injector: Injector,
private _viewContainerRef: ViewContainerRef,
@Inject(DOCUMENT) private _document: any) {}
@Inject(DOCUMENT) private _document: any,
private _changeDetectorRef?: ChangeDetectorRef) {}
/**
* Attaches the content with a particular context.
@@ -63,6 +65,17 @@ export class MatMenuContent implements OnDestroy {
// own `OverlayRef` panel), we have to re-insert the host element every time, otherwise we
// risk it staying attached to a pane that's no longer in the DOM.
element.parentNode!.insertBefore(this._outlet.outletElement, element);
// When `MatMenuContent` is used in an `OnPush` component, the insertion of the menu
// content via `createEmbeddedView` does not cause the content to be seen as "dirty"
// by Angular. This causes the `@ContentChildren` for menu items within the menu to
// not be updated by Angular. By explicitly marking for check here, we tell Angular that
// it needs to check for new menu items and update the `@ContentChild` in `MatMenu`.
// @breaking-change 9.0.0 Make change detector ref required
if (this._changeDetectorRef) {
this._changeDetectorRef.markForCheck();
}
this._portal.attach(this._outlet, context);
this._attached.next();
}
+68 -26
View File
@@ -1,48 +1,49 @@
import {ComponentFixture, fakeAsync, flush, inject, TestBed, tick} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {FocusMonitor} from '@angular/cdk/a11y';
import {Direction, Directionality} from '@angular/cdk/bidi';
import {DOWN_ARROW, END, ESCAPE, HOME, LEFT_ARROW, RIGHT_ARROW, TAB} from '@angular/cdk/keycodes';
import {Overlay, OverlayContainer} from '@angular/cdk/overlay';
import {ScrollDispatcher} from '@angular/cdk/scrolling';
import {
createKeyboardEvent,
createMouseEvent,
dispatchEvent,
dispatchFakeEvent,
dispatchKeyboardEvent,
dispatchMouseEvent,
MockNgZone,
patchElementFocus,
} from '@angular/cdk/testing';
import {
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
Input,
Output,
NgZone,
Output,
Provider,
QueryList,
TemplateRef,
Type,
ViewChild,
ViewChildren,
QueryList,
Type,
Provider,
} from '@angular/core';
import {Direction, Directionality} from '@angular/cdk/bidi';
import {OverlayContainer, Overlay} from '@angular/cdk/overlay';
import {ESCAPE, LEFT_ARROW, RIGHT_ARROW, DOWN_ARROW, TAB, HOME, END} from '@angular/cdk/keycodes';
import {ComponentFixture, fakeAsync, flush, inject, TestBed, tick} from '@angular/core/testing';
import {MatRipple} from '@angular/material/core';
import {By} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {Subject} from 'rxjs';
import {
MAT_MENU_DEFAULT_OPTIONS,
MatMenu,
MatMenuItem,
MatMenuModule,
MatMenuPanel,
MatMenuTrigger,
MenuPositionX,
MenuPositionY,
MatMenuItem,
} from './index';
import {MENU_PANEL_TOP_PADDING, MAT_MENU_SCROLL_STRATEGY} from './menu-trigger';
import {MatRipple} from '@angular/material/core';
import {
dispatchKeyboardEvent,
dispatchMouseEvent,
dispatchEvent,
createKeyboardEvent,
createMouseEvent,
dispatchFakeEvent,
patchElementFocus,
MockNgZone,
} from '@angular/cdk/testing';
import {Subject} from 'rxjs';
import {ScrollDispatcher} from '@angular/cdk/scrolling';
import {FocusMonitor} from '@angular/cdk/a11y';
import {MAT_MENU_SCROLL_STRATEGY, MENU_PANEL_TOP_PADDING} from './menu-trigger';
describe('MatMenu', () => {
@@ -892,6 +893,25 @@ describe('MatMenu', () => {
expect(document.activeElement).toBe(items[1], 'Expected second item to be focused');
flush();
}));
it('should open submenus when the menu is inside an OnPush component', fakeAsync(() => {
const fixture = createComponent(LazyMenuWithOnPush);
fixture.detectChanges();
// Open the top-level menu
fixture.componentInstance.rootTrigger.nativeElement.click();
fixture.detectChanges();
flush();
// Dispatch a `mouseenter` on the menu item to open the submenu.
// This will only work if the top-level menu is aware the this menu item exists.
dispatchMouseEvent(fixture.componentInstance.menuItemWithSubmenu.nativeElement, 'mouseenter');
fixture.detectChanges();
flush();
expect(overlayContainerElement.querySelectorAll('.mat-menu-item').length)
.toBe(2, 'Expected two open menus');
}));
});
describe('positions', () => {
@@ -2373,3 +2393,25 @@ class SimpleMenuWithRepeater {
items = ['Pizza', 'Pasta'];
}
@Component({
template: `
<button [matMenuTriggerFor]="menu" #triggerEl>Toggle menu</button>
<mat-menu #menu="matMenu">
<ng-template matMenuContent>
<button [matMenuTriggerFor]="menu2" mat-menu-item #menuItem>Item</button>
</ng-template>
</mat-menu>
<mat-menu #menu2="matMenu">
<ng-template matMenuContent>
<button mat-menu-item #subMenuItem>Sub item</button>
</ng-template>
</mat-menu>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
class LazyMenuWithOnPush {
@ViewChild('triggerEl', {static: false, read: ElementRef}) rootTrigger: ElementRef;
@ViewChild('menuItem', {static: false, read: ElementRef}) menuItemWithSubmenu: ElementRef;
}
+1 -1
View File
@@ -63,7 +63,7 @@ export declare const matMenuAnimations: {
export declare class MatMenuContent implements OnDestroy {
_attached: Subject<void>;
constructor(_template: TemplateRef<any>, _componentFactoryResolver: ComponentFactoryResolver, _appRef: ApplicationRef, _injector: Injector, _viewContainerRef: ViewContainerRef, _document: any);
constructor(_template: TemplateRef<any>, _componentFactoryResolver: ComponentFactoryResolver, _appRef: ApplicationRef, _injector: Injector, _viewContainerRef: ViewContainerRef, _document: any, _changeDetectorRef?: ChangeDetectorRef | undefined);
attach(context?: any): void;
detach(): void;
ngOnDestroy(): void;