mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user