Skip to content

fix(cdk/menu): close sibling triggers when opening a menu #30894

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions goldens/cdk/menu/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnChanges, OnD
// @public
export abstract class CdkMenuTriggerBase implements OnDestroy {
protected childMenu?: Menu;
abstract close(): void;
readonly closed: EventEmitter<void>;
protected readonly destroyed: Subject<void>;
protected getMenuContentPortal(): TemplatePortal<any>;
Expand Down Expand Up @@ -273,15 +274,6 @@ export type ContextMenuCoordinates = {
y: number;
};

// @public
export class ContextMenuTracker {
update(trigger: CdkContextMenuTrigger): void;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<ContextMenuTracker, never>;
// (undocumented)
static ɵprov: i0.ɵɵInjectableDeclaration<ContextMenuTracker>;
}

// @public
export interface FocusableElement {
_elementRef: ElementRef<HTMLElement>;
Expand Down Expand Up @@ -358,6 +350,15 @@ export interface MenuStackItem {
menuStack?: MenuStack;
}

// @public
export class MenuTracker {
update(trigger: CdkMenuTriggerBase): void;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<MenuTracker, never>;
// (undocumented)
static ɵprov: i0.ɵɵInjectableDeclaration<MenuTracker>;
}

// @public
export const PARENT_OR_NEW_INLINE_MENU_STACK_PROVIDER: (orientation: "vertical" | "horizontal") => {
provide: InjectionToken<MenuStack>;
Expand Down
27 changes: 4 additions & 23 deletions src/cdk/menu/context-menu-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
ChangeDetectorRef,
Directive,
inject,
Injectable,
Input,
OnDestroy,
} from '@angular/core';
Expand All @@ -26,7 +25,7 @@ import {_getEventTarget} from '../platform';
import {merge, partition} from 'rxjs';
import {skip, takeUntil, skipWhile} from 'rxjs/operators';
import {MENU_STACK, MenuStack} from './menu-stack';
import {CdkMenuTriggerBase, MENU_TRIGGER} from './menu-trigger-base';
import {CdkMenuTriggerBase, MENU_TRIGGER, MenuTracker} from './menu-trigger-base';

/** The preferred menu positions for the context menu. */
const CONTEXT_MENU_POSITIONS = STANDARD_DROPDOWN_BELOW_POSITIONS.map(position => {
Expand All @@ -37,24 +36,6 @@ const CONTEXT_MENU_POSITIONS = STANDARD_DROPDOWN_BELOW_POSITIONS.map(position =>
return {...position, offsetX, offsetY};
});

/** Tracks the last open context menu trigger across the entire application. */
@Injectable({providedIn: 'root'})
export class ContextMenuTracker {
/** The last open context menu trigger. */
private static _openContextMenuTrigger?: CdkContextMenuTrigger;

/**
* Close the previous open context menu and set the given one as being open.
* @param trigger The trigger for the currently open Context Menu.
*/
update(trigger: CdkContextMenuTrigger) {
if (ContextMenuTracker._openContextMenuTrigger !== trigger) {
ContextMenuTracker._openContextMenuTrigger?.close();
ContextMenuTracker._openContextMenuTrigger = trigger;
}
}
}

/** The coordinates where the context menu should open. */
export type ContextMenuCoordinates = {x: number; y: number};

Expand Down Expand Up @@ -87,8 +68,8 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr
/** The directionality of the page. */
private readonly _directionality = inject(Directionality, {optional: true});

/** The app's context menu tracking registry */
private readonly _contextMenuTracker = inject(ContextMenuTracker);
/** The app's menu tracking registry */
private readonly _menuTracker = inject(MenuTracker);

private readonly _changeDetectorRef = inject(ChangeDetectorRef);

Expand Down Expand Up @@ -128,7 +109,7 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr
// resulting in multiple stacked context menus being displayed.
event.stopPropagation();

this._contextMenuTracker.update(this);
this._menuTracker.update(this);
this._open(event, {x: event.clientX, y: event.clientY});

// A context menu can be triggered via a mouse right click or a keyboard shortcut.
Expand Down
22 changes: 22 additions & 0 deletions src/cdk/menu/menu-trigger-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Directive,
EventEmitter,
inject,
Injectable,
InjectionToken,
Injector,
OnDestroy,
Expand Down Expand Up @@ -37,6 +38,24 @@ export const MENU_SCROLL_STRATEGY = new InjectionToken<() => ScrollStrategy>(
},
);

/** Tracks the last open menu trigger across the entire application. */
@Injectable({providedIn: 'root'})
export class MenuTracker {
/** The last open menu trigger. */
private static _openMenuTrigger?: CdkMenuTriggerBase;

/**
* Close the previous open menu and set the given one as being open.
* @param trigger The trigger for the currently open Menu.
*/
update(trigger: CdkMenuTriggerBase) {
if (MenuTracker._openMenuTrigger !== trigger) {
MenuTracker._openMenuTrigger?.close();
MenuTracker._openMenuTrigger = trigger;
}
}
}

/**
* Abstract directive that implements shared logic common to all menu triggers.
* This class can be extended to create custom menu trigger types.
Expand Down Expand Up @@ -78,6 +97,9 @@ export abstract class CdkMenuTriggerBase implements OnDestroy {
/** Context data to be passed along to the menu template */
menuData: unknown;

/** Close the opened menu. */
abstract close(): void;

/** A reference to the overlay which manages the triggered menu */
protected overlayRef: OverlayRef | null = null;

Expand Down
8 changes: 7 additions & 1 deletion src/cdk/menu/menu-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {takeUntil} from 'rxjs/operators';
import {CDK_MENU, Menu} from './menu-interface';
import {PARENT_OR_NEW_MENU_STACK_PROVIDER} from './menu-stack';
import {MENU_AIM} from './menu-aim';
import {CdkMenuTriggerBase, MENU_TRIGGER} from './menu-trigger-base';
import {CdkMenuTriggerBase, MENU_TRIGGER, MenuTracker} from './menu-trigger-base';
import {eventDispatchesNativeClick} from './event-detection';

/**
Expand Down Expand Up @@ -84,6 +84,9 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnChanges, OnD
private readonly _renderer = inject(Renderer2);
private _cleanupMouseenter: () => void;

/** The app's menu tracking registry */
private readonly _menuTracker = inject(MenuTracker);

/** The parent menu this trigger belongs to. */
private readonly _parentMenu = inject(CDK_MENU, {optional: true});

Expand All @@ -107,6 +110,9 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnChanges, OnD

/** Open the attached menu. */
open() {
if (!this._parentMenu) {
this._menuTracker.update(this);
}
if (!this.isOpen() && this.menuTemplateRef != null) {
this.opened.next();

Expand Down
Loading