624 lines
21 KiB
TypeScript
624 lines
21 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.dev/license
|
|
*/
|
|
|
|
import {
|
|
FocusMonitor,
|
|
FocusOrigin,
|
|
isFakeMousedownFromScreenReader,
|
|
isFakeTouchstartFromScreenReader,
|
|
} from '@angular/cdk/a11y';
|
|
import {Direction, Directionality} from '@angular/cdk/bidi';
|
|
import {ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes';
|
|
import {
|
|
FlexibleConnectedPositionStrategy,
|
|
HorizontalConnectionPos,
|
|
Overlay,
|
|
OverlayConfig,
|
|
OverlayRef,
|
|
ScrollStrategy,
|
|
VerticalConnectionPos,
|
|
} from '@angular/cdk/overlay';
|
|
import {TemplatePortal} from '@angular/cdk/portal';
|
|
import {
|
|
AfterContentInit,
|
|
ChangeDetectorRef,
|
|
Directive,
|
|
ElementRef,
|
|
EventEmitter,
|
|
inject,
|
|
InjectionToken,
|
|
Input,
|
|
NgZone,
|
|
OnDestroy,
|
|
Output,
|
|
ViewContainerRef,
|
|
} from '@angular/core';
|
|
import {normalizePassiveListenerOptions} from '@angular/cdk/platform';
|
|
import {asapScheduler, merge, Observable, of as observableOf, Subscription} from 'rxjs';
|
|
import {delay, filter, take, takeUntil} from 'rxjs/operators';
|
|
import {MatMenu, MenuCloseReason} from './menu';
|
|
import {throwMatMenuRecursiveError} from './menu-errors';
|
|
import {MatMenuItem} from './menu-item';
|
|
import {MAT_MENU_PANEL, MatMenuPanel} from './menu-panel';
|
|
import {MenuPositionX, MenuPositionY} from './menu-positions';
|
|
|
|
/** Injection token that determines the scroll handling while the menu is open. */
|
|
export const MAT_MENU_SCROLL_STRATEGY = new InjectionToken<() => ScrollStrategy>(
|
|
'mat-menu-scroll-strategy',
|
|
{
|
|
providedIn: 'root',
|
|
factory: () => {
|
|
const overlay = inject(Overlay);
|
|
return () => overlay.scrollStrategies.reposition();
|
|
},
|
|
},
|
|
);
|
|
|
|
/** @docs-private */
|
|
export function MAT_MENU_SCROLL_STRATEGY_FACTORY(overlay: Overlay): () => ScrollStrategy {
|
|
return () => overlay.scrollStrategies.reposition();
|
|
}
|
|
|
|
/** @docs-private */
|
|
export const MAT_MENU_SCROLL_STRATEGY_FACTORY_PROVIDER = {
|
|
provide: MAT_MENU_SCROLL_STRATEGY,
|
|
deps: [Overlay],
|
|
useFactory: MAT_MENU_SCROLL_STRATEGY_FACTORY,
|
|
};
|
|
|
|
/** Options for binding a passive event listener. */
|
|
const passiveEventListenerOptions = normalizePassiveListenerOptions({passive: true});
|
|
|
|
/**
|
|
* Default top padding of the menu panel.
|
|
* @deprecated No longer being used. Will be removed.
|
|
* @breaking-change 15.0.0
|
|
*/
|
|
export const MENU_PANEL_TOP_PADDING = 8;
|
|
|
|
/** Directive applied to an element that should trigger a `mat-menu`. */
|
|
@Directive({
|
|
selector: `[mat-menu-trigger-for], [matMenuTriggerFor]`,
|
|
host: {
|
|
'class': 'mat-mdc-menu-trigger',
|
|
'[attr.aria-haspopup]': 'menu ? "menu" : null',
|
|
'[attr.aria-expanded]': 'menuOpen',
|
|
'[attr.aria-controls]': 'menuOpen ? menu.panelId : null',
|
|
'(click)': '_handleClick($event)',
|
|
'(mousedown)': '_handleMousedown($event)',
|
|
'(keydown)': '_handleKeydown($event)',
|
|
},
|
|
exportAs: 'matMenuTrigger',
|
|
})
|
|
export class MatMenuTrigger implements AfterContentInit, OnDestroy {
|
|
private _overlay = inject(Overlay);
|
|
private _element = inject<ElementRef<HTMLElement>>(ElementRef);
|
|
private _viewContainerRef = inject(ViewContainerRef);
|
|
private _menuItemInstance = inject(MatMenuItem, {optional: true, self: true})!;
|
|
private _dir = inject(Directionality, {optional: true});
|
|
private _focusMonitor = inject(FocusMonitor);
|
|
private _ngZone = inject(NgZone);
|
|
private _scrollStrategy = inject(MAT_MENU_SCROLL_STRATEGY);
|
|
private _changeDetectorRef = inject(ChangeDetectorRef);
|
|
|
|
private _portal: TemplatePortal;
|
|
private _overlayRef: OverlayRef | null = null;
|
|
private _menuOpen: boolean = false;
|
|
private _closingActionsSubscription = Subscription.EMPTY;
|
|
private _hoverSubscription = Subscription.EMPTY;
|
|
private _menuCloseSubscription = Subscription.EMPTY;
|
|
|
|
/**
|
|
* We're specifically looking for a `MatMenu` here since the generic `MatMenuPanel`
|
|
* interface lacks some functionality around nested menus and animations.
|
|
*/
|
|
private _parentMaterialMenu: MatMenu | undefined;
|
|
|
|
/**
|
|
* Cached value of the padding of the parent menu panel.
|
|
* Used to offset sub-menus to compensate for the padding.
|
|
*/
|
|
private _parentInnerPadding: number | undefined;
|
|
|
|
/**
|
|
* Handles touch start events on the trigger.
|
|
* Needs to be an arrow function so we can easily use addEventListener and removeEventListener.
|
|
*/
|
|
private _handleTouchStart = (event: TouchEvent) => {
|
|
if (!isFakeTouchstartFromScreenReader(event)) {
|
|
this._openedBy = 'touch';
|
|
}
|
|
};
|
|
|
|
// Tracking input type is necessary so it's possible to only auto-focus
|
|
// the first item of the list when the menu is opened via the keyboard
|
|
_openedBy: Exclude<FocusOrigin, 'program' | null> | undefined = undefined;
|
|
|
|
/**
|
|
* @deprecated
|
|
* @breaking-change 8.0.0
|
|
*/
|
|
@Input('mat-menu-trigger-for')
|
|
get _deprecatedMatMenuTriggerFor(): MatMenuPanel | null {
|
|
return this.menu;
|
|
}
|
|
set _deprecatedMatMenuTriggerFor(v: MatMenuPanel | null) {
|
|
this.menu = v;
|
|
}
|
|
|
|
/** References the menu instance that the trigger is associated with. */
|
|
@Input('matMenuTriggerFor')
|
|
get menu(): MatMenuPanel | null {
|
|
return this._menu;
|
|
}
|
|
set menu(menu: MatMenuPanel | null) {
|
|
if (menu === this._menu) {
|
|
return;
|
|
}
|
|
|
|
this._menu = menu;
|
|
this._menuCloseSubscription.unsubscribe();
|
|
|
|
if (menu) {
|
|
if (menu === this._parentMaterialMenu && (typeof ngDevMode === 'undefined' || ngDevMode)) {
|
|
throwMatMenuRecursiveError();
|
|
}
|
|
|
|
this._menuCloseSubscription = menu.close.subscribe((reason: MenuCloseReason) => {
|
|
this._destroyMenu(reason);
|
|
|
|
// If a click closed the menu, we should close the entire chain of nested menus.
|
|
if ((reason === 'click' || reason === 'tab') && this._parentMaterialMenu) {
|
|
this._parentMaterialMenu.closed.emit(reason);
|
|
}
|
|
});
|
|
}
|
|
|
|
this._menuItemInstance?._setTriggersSubmenu(this.triggersSubmenu());
|
|
}
|
|
private _menu: MatMenuPanel | null;
|
|
|
|
/** Data to be passed along to any lazily-rendered content. */
|
|
@Input('matMenuTriggerData') menuData: any;
|
|
|
|
/**
|
|
* Whether focus should be restored when the menu is closed.
|
|
* Note that disabling this option can have accessibility implications
|
|
* and it's up to you to manage focus, if you decide to turn it off.
|
|
*/
|
|
@Input('matMenuTriggerRestoreFocus') restoreFocus: boolean = true;
|
|
|
|
/** Event emitted when the associated menu is opened. */
|
|
@Output() readonly menuOpened: EventEmitter<void> = new EventEmitter<void>();
|
|
|
|
/**
|
|
* Event emitted when the associated menu is opened.
|
|
* @deprecated Switch to `menuOpened` instead
|
|
* @breaking-change 8.0.0
|
|
*/
|
|
// tslint:disable-next-line:no-output-on-prefix
|
|
@Output() readonly onMenuOpen: EventEmitter<void> = this.menuOpened;
|
|
|
|
/** Event emitted when the associated menu is closed. */
|
|
@Output() readonly menuClosed: EventEmitter<void> = new EventEmitter<void>();
|
|
|
|
/**
|
|
* Event emitted when the associated menu is closed.
|
|
* @deprecated Switch to `menuClosed` instead
|
|
* @breaking-change 8.0.0
|
|
*/
|
|
// tslint:disable-next-line:no-output-on-prefix
|
|
@Output() readonly onMenuClose: EventEmitter<void> = this.menuClosed;
|
|
|
|
constructor(...args: unknown[]);
|
|
|
|
constructor() {
|
|
const parentMenu = inject<MatMenuPanel>(MAT_MENU_PANEL, {optional: true});
|
|
|
|
this._parentMaterialMenu = parentMenu instanceof MatMenu ? parentMenu : undefined;
|
|
|
|
this._element.nativeElement.addEventListener(
|
|
'touchstart',
|
|
this._handleTouchStart,
|
|
passiveEventListenerOptions,
|
|
);
|
|
}
|
|
|
|
ngAfterContentInit() {
|
|
this._handleHover();
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
if (this._overlayRef) {
|
|
this._overlayRef.dispose();
|
|
this._overlayRef = null;
|
|
}
|
|
|
|
this._element.nativeElement.removeEventListener(
|
|
'touchstart',
|
|
this._handleTouchStart,
|
|
passiveEventListenerOptions,
|
|
);
|
|
|
|
this._menuCloseSubscription.unsubscribe();
|
|
this._closingActionsSubscription.unsubscribe();
|
|
this._hoverSubscription.unsubscribe();
|
|
}
|
|
|
|
/** Whether the menu is open. */
|
|
get menuOpen(): boolean {
|
|
return this._menuOpen;
|
|
}
|
|
|
|
/** The text direction of the containing app. */
|
|
get dir(): Direction {
|
|
return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr';
|
|
}
|
|
|
|
/** Whether the menu triggers a sub-menu or a top-level one. */
|
|
triggersSubmenu(): boolean {
|
|
return !!(this._menuItemInstance && this._parentMaterialMenu && this.menu);
|
|
}
|
|
|
|
/** Toggles the menu between the open and closed states. */
|
|
toggleMenu(): void {
|
|
return this._menuOpen ? this.closeMenu() : this.openMenu();
|
|
}
|
|
|
|
/** Opens the menu. */
|
|
openMenu(): void {
|
|
const menu = this.menu;
|
|
|
|
if (this._menuOpen || !menu) {
|
|
return;
|
|
}
|
|
|
|
const overlayRef = this._createOverlay(menu);
|
|
const overlayConfig = overlayRef.getConfig();
|
|
const positionStrategy = overlayConfig.positionStrategy as FlexibleConnectedPositionStrategy;
|
|
|
|
this._setPosition(menu, positionStrategy);
|
|
overlayConfig.hasBackdrop =
|
|
menu.hasBackdrop == null ? !this.triggersSubmenu() : menu.hasBackdrop;
|
|
overlayRef.attach(this._getPortal(menu));
|
|
|
|
if (menu.lazyContent) {
|
|
menu.lazyContent.attach(this.menuData);
|
|
}
|
|
|
|
this._closingActionsSubscription = this._menuClosingActions().subscribe(() => this.closeMenu());
|
|
this._initMenu(menu);
|
|
|
|
if (menu instanceof MatMenu) {
|
|
menu._startAnimation();
|
|
menu._directDescendantItems.changes.pipe(takeUntil(menu.close)).subscribe(() => {
|
|
// Re-adjust the position without locking when the amount of items
|
|
// changes so that the overlay is allowed to pick a new optimal position.
|
|
positionStrategy.withLockedPosition(false).reapplyLastPosition();
|
|
positionStrategy.withLockedPosition(true);
|
|
});
|
|
}
|
|
}
|
|
|
|
/** Closes the menu. */
|
|
closeMenu(): void {
|
|
this.menu?.close.emit();
|
|
}
|
|
|
|
/**
|
|
* Focuses the menu trigger.
|
|
* @param origin Source of the menu trigger's focus.
|
|
*/
|
|
focus(origin?: FocusOrigin, options?: FocusOptions) {
|
|
if (this._focusMonitor && origin) {
|
|
this._focusMonitor.focusVia(this._element, origin, options);
|
|
} else {
|
|
this._element.nativeElement.focus(options);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the position of the menu to ensure that it fits all options within the viewport.
|
|
*/
|
|
updatePosition(): void {
|
|
this._overlayRef?.updatePosition();
|
|
}
|
|
|
|
/** Closes the menu and does the necessary cleanup. */
|
|
private _destroyMenu(reason: MenuCloseReason) {
|
|
if (!this._overlayRef || !this.menuOpen) {
|
|
return;
|
|
}
|
|
|
|
const menu = this.menu;
|
|
this._closingActionsSubscription.unsubscribe();
|
|
this._overlayRef.detach();
|
|
|
|
// Always restore focus if the user is navigating using the keyboard or the menu was opened
|
|
// programmatically. We don't restore for non-root triggers, because it can prevent focus
|
|
// from making it back to the root trigger when closing a long chain of menus by clicking
|
|
// on the backdrop.
|
|
if (this.restoreFocus && (reason === 'keydown' || !this._openedBy || !this.triggersSubmenu())) {
|
|
this.focus(this._openedBy);
|
|
}
|
|
|
|
this._openedBy = undefined;
|
|
|
|
if (menu instanceof MatMenu) {
|
|
menu._resetAnimation();
|
|
|
|
if (menu.lazyContent) {
|
|
// Wait for the exit animation to finish before detaching the content.
|
|
menu._animationDone
|
|
.pipe(
|
|
filter(event => event.toState === 'void'),
|
|
take(1),
|
|
// Interrupt if the content got re-attached.
|
|
takeUntil(menu.lazyContent._attached),
|
|
)
|
|
.subscribe({
|
|
next: () => menu.lazyContent!.detach(),
|
|
// No matter whether the content got re-attached, reset the menu.
|
|
complete: () => this._setIsMenuOpen(false),
|
|
});
|
|
} else {
|
|
this._setIsMenuOpen(false);
|
|
}
|
|
} else {
|
|
this._setIsMenuOpen(false);
|
|
menu?.lazyContent?.detach();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method sets the menu state to open and focuses the first item if
|
|
* the menu was opened via the keyboard.
|
|
*/
|
|
private _initMenu(menu: MatMenuPanel): void {
|
|
menu.parentMenu = this.triggersSubmenu() ? this._parentMaterialMenu : undefined;
|
|
menu.direction = this.dir;
|
|
menu.focusFirstItem(this._openedBy || 'program');
|
|
this._setIsMenuOpen(true);
|
|
}
|
|
|
|
// set state rather than toggle to support triggers sharing a menu
|
|
private _setIsMenuOpen(isOpen: boolean): void {
|
|
if (isOpen !== this._menuOpen) {
|
|
this._menuOpen = isOpen;
|
|
this._menuOpen ? this.menuOpened.emit() : this.menuClosed.emit();
|
|
|
|
if (this.triggersSubmenu()) {
|
|
this._menuItemInstance._setHighlighted(isOpen);
|
|
}
|
|
|
|
this._changeDetectorRef.markForCheck();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method creates the overlay from the provided menu's template and saves its
|
|
* OverlayRef so that it can be attached to the DOM when openMenu is called.
|
|
*/
|
|
private _createOverlay(menu: MatMenuPanel): OverlayRef {
|
|
if (!this._overlayRef) {
|
|
const config = this._getOverlayConfig(menu);
|
|
this._subscribeToPositions(
|
|
menu,
|
|
config.positionStrategy as FlexibleConnectedPositionStrategy,
|
|
);
|
|
this._overlayRef = this._overlay.create(config);
|
|
this._overlayRef.keydownEvents().subscribe(event => {
|
|
if (this.menu instanceof MatMenu) {
|
|
this.menu._handleKeydown(event);
|
|
}
|
|
});
|
|
}
|
|
|
|
return this._overlayRef;
|
|
}
|
|
|
|
/**
|
|
* This method builds the configuration object needed to create the overlay, the OverlayState.
|
|
* @returns OverlayConfig
|
|
*/
|
|
private _getOverlayConfig(menu: MatMenuPanel): OverlayConfig {
|
|
return new OverlayConfig({
|
|
positionStrategy: this._overlay
|
|
.position()
|
|
.flexibleConnectedTo(this._element)
|
|
.withLockedPosition()
|
|
.withGrowAfterOpen()
|
|
.withTransformOriginOn('.mat-menu-panel, .mat-mdc-menu-panel'),
|
|
backdropClass: menu.backdropClass || 'cdk-overlay-transparent-backdrop',
|
|
panelClass: menu.overlayPanelClass,
|
|
scrollStrategy: this._scrollStrategy(),
|
|
direction: this._dir || 'ltr',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Listens to changes in the position of the overlay and sets the correct classes
|
|
* on the menu based on the new position. This ensures the animation origin is always
|
|
* correct, even if a fallback position is used for the overlay.
|
|
*/
|
|
private _subscribeToPositions(menu: MatMenuPanel, position: FlexibleConnectedPositionStrategy) {
|
|
if (menu.setPositionClasses) {
|
|
position.positionChanges.subscribe(change => {
|
|
this._ngZone.run(() => {
|
|
const posX: MenuPositionX =
|
|
change.connectionPair.overlayX === 'start' ? 'after' : 'before';
|
|
const posY: MenuPositionY = change.connectionPair.overlayY === 'top' ? 'below' : 'above';
|
|
menu.setPositionClasses!(posX, posY);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the appropriate positions on a position strategy
|
|
* so the overlay connects with the trigger correctly.
|
|
* @param positionStrategy Strategy whose position to update.
|
|
*/
|
|
private _setPosition(menu: MatMenuPanel, positionStrategy: FlexibleConnectedPositionStrategy) {
|
|
let [originX, originFallbackX]: HorizontalConnectionPos[] =
|
|
menu.xPosition === 'before' ? ['end', 'start'] : ['start', 'end'];
|
|
|
|
let [overlayY, overlayFallbackY]: VerticalConnectionPos[] =
|
|
menu.yPosition === 'above' ? ['bottom', 'top'] : ['top', 'bottom'];
|
|
|
|
let [originY, originFallbackY] = [overlayY, overlayFallbackY];
|
|
let [overlayX, overlayFallbackX] = [originX, originFallbackX];
|
|
let offsetY = 0;
|
|
|
|
if (this.triggersSubmenu()) {
|
|
// When the menu is a sub-menu, it should always align itself
|
|
// to the edges of the trigger, instead of overlapping it.
|
|
overlayFallbackX = originX = menu.xPosition === 'before' ? 'start' : 'end';
|
|
originFallbackX = overlayX = originX === 'end' ? 'start' : 'end';
|
|
|
|
if (this._parentMaterialMenu) {
|
|
if (this._parentInnerPadding == null) {
|
|
const firstItem = this._parentMaterialMenu.items.first;
|
|
this._parentInnerPadding = firstItem ? firstItem._getHostElement().offsetTop : 0;
|
|
}
|
|
|
|
offsetY = overlayY === 'bottom' ? this._parentInnerPadding : -this._parentInnerPadding;
|
|
}
|
|
} else if (!menu.overlapTrigger) {
|
|
originY = overlayY === 'top' ? 'bottom' : 'top';
|
|
originFallbackY = overlayFallbackY === 'top' ? 'bottom' : 'top';
|
|
}
|
|
|
|
positionStrategy.withPositions([
|
|
{originX, originY, overlayX, overlayY, offsetY},
|
|
{originX: originFallbackX, originY, overlayX: overlayFallbackX, overlayY, offsetY},
|
|
{
|
|
originX,
|
|
originY: originFallbackY,
|
|
overlayX,
|
|
overlayY: overlayFallbackY,
|
|
offsetY: -offsetY,
|
|
},
|
|
{
|
|
originX: originFallbackX,
|
|
originY: originFallbackY,
|
|
overlayX: overlayFallbackX,
|
|
overlayY: overlayFallbackY,
|
|
offsetY: -offsetY,
|
|
},
|
|
]);
|
|
}
|
|
|
|
/** Returns a stream that emits whenever an action that should close the menu occurs. */
|
|
private _menuClosingActions() {
|
|
const backdrop = this._overlayRef!.backdropClick();
|
|
const detachments = this._overlayRef!.detachments();
|
|
const parentClose = this._parentMaterialMenu ? this._parentMaterialMenu.closed : observableOf();
|
|
const hover = this._parentMaterialMenu
|
|
? this._parentMaterialMenu._hovered().pipe(
|
|
filter(active => active !== this._menuItemInstance),
|
|
filter(() => this._menuOpen),
|
|
)
|
|
: observableOf();
|
|
|
|
return merge(backdrop, parentClose as Observable<MenuCloseReason>, hover, detachments);
|
|
}
|
|
|
|
/** Handles mouse presses on the trigger. */
|
|
_handleMousedown(event: MouseEvent): void {
|
|
if (!isFakeMousedownFromScreenReader(event)) {
|
|
// Since right or middle button clicks won't trigger the `click` event,
|
|
// we shouldn't consider the menu as opened by mouse in those cases.
|
|
this._openedBy = event.button === 0 ? 'mouse' : undefined;
|
|
|
|
// Since clicking on the trigger won't close the menu if it opens a sub-menu,
|
|
// we should prevent focus from moving onto it via click to avoid the
|
|
// highlight from lingering on the menu item.
|
|
if (this.triggersSubmenu()) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Handles key presses on the trigger. */
|
|
_handleKeydown(event: KeyboardEvent): void {
|
|
const keyCode = event.keyCode;
|
|
|
|
// Pressing enter on the trigger will trigger the click handler later.
|
|
if (keyCode === ENTER || keyCode === SPACE) {
|
|
this._openedBy = 'keyboard';
|
|
}
|
|
|
|
if (
|
|
this.triggersSubmenu() &&
|
|
((keyCode === RIGHT_ARROW && this.dir === 'ltr') ||
|
|
(keyCode === LEFT_ARROW && this.dir === 'rtl'))
|
|
) {
|
|
this._openedBy = 'keyboard';
|
|
this.openMenu();
|
|
}
|
|
}
|
|
|
|
/** Handles click events on the trigger. */
|
|
_handleClick(event: MouseEvent): void {
|
|
if (this.triggersSubmenu()) {
|
|
// Stop event propagation to avoid closing the parent menu.
|
|
event.stopPropagation();
|
|
this.openMenu();
|
|
} else {
|
|
this.toggleMenu();
|
|
}
|
|
}
|
|
|
|
/** Handles the cases where the user hovers over the trigger. */
|
|
private _handleHover() {
|
|
// Subscribe to changes in the hovered item in order to toggle the panel.
|
|
if (!this.triggersSubmenu() || !this._parentMaterialMenu) {
|
|
return;
|
|
}
|
|
|
|
this._hoverSubscription = this._parentMaterialMenu
|
|
._hovered()
|
|
// Since we might have multiple competing triggers for the same menu (e.g. a sub-menu
|
|
// with different data and triggers), we have to delay it by a tick to ensure that
|
|
// it won't be closed immediately after it is opened.
|
|
.pipe(
|
|
filter(active => active === this._menuItemInstance && !active.disabled),
|
|
delay(0, asapScheduler),
|
|
)
|
|
.subscribe(() => {
|
|
this._openedBy = 'mouse';
|
|
|
|
// If the same menu is used between multiple triggers, it might still be animating
|
|
// while the new trigger tries to re-open it. Wait for the animation to finish
|
|
// before doing so. Also interrupt if the user moves to another item.
|
|
if (this.menu instanceof MatMenu && this.menu._isAnimating) {
|
|
// We need the `delay(0)` here in order to avoid
|
|
// 'changed after checked' errors in some cases. See #12194.
|
|
this.menu._animationDone
|
|
.pipe(take(1), delay(0, asapScheduler), takeUntil(this._parentMaterialMenu!._hovered()))
|
|
.subscribe(() => this.openMenu());
|
|
} else {
|
|
this.openMenu();
|
|
}
|
|
});
|
|
}
|
|
|
|
/** Gets the portal that should be attached to the overlay. */
|
|
private _getPortal(menu: MatMenuPanel): 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 !== menu.templateRef) {
|
|
this._portal = new TemplatePortal(menu.templateRef, this._viewContainerRef);
|
|
}
|
|
|
|
return this._portal;
|
|
}
|
|
}
|