diff --git a/src/cdk/menu/context-menu-trigger.spec.ts b/src/cdk/menu/context-menu-trigger.spec.ts index 99757aaa2737..79f2ec79d5a8 100644 --- a/src/cdk/menu/context-menu-trigger.spec.ts +++ b/src/cdk/menu/context-menu-trigger.spec.ts @@ -143,6 +143,37 @@ describe('CdkContextMenuTrigger', () => { fixture.detectChanges(); expect(getContextMenu()).toBeDefined(); }); + + it('should stay open with disable close on outside click', () => { + fixture.componentInstance.shoudCloseOnOutsideClicks = true; + openContextMenu(); + expect(getContextMenu()).toBeDefined(); + + fixture.nativeElement.querySelector('#other').click(); + fixture.detectChanges(); + expect(getContextMenu()).toBeDefined(); + }); + + it('should emit that menu had click outside of it', () => { + openContextMenu(); + expect(getContextMenu()).toBeDefined(); + spyOn(fixture.componentInstance.trigger.outsideClicked, 'emit'); + + fixture.nativeElement.querySelector('#other').click(); + fixture.detectChanges(); + + expect(fixture.componentInstance.trigger.outsideClicked.emit).toHaveBeenCalled(); + expect(getContextMenu()).not.toBeDefined(); + }); + + it('should emit that menu was triggered', () => { + fixture.detectChanges(); + spyOn(fixture.componentInstance.trigger.triggered, 'emit'); + + openContextMenu(); + + expect(fixture.componentInstance.trigger.triggered.emit).toHaveBeenCalled(); + }); }); describe('nested context menu triggers', () => { @@ -442,7 +473,8 @@ describe('CdkContextMenuTrigger', () => { @Component({ template: ` -
+
@@ -459,6 +491,8 @@ class SimpleContextMenu { @ViewChild(CdkMenu, {read: ElementRef}) nativeMenu?: ElementRef; @ViewChildren(CdkMenu) menus: QueryList; + + shoudCloseOnOutsideClicks = false; } @Component({ diff --git a/src/cdk/menu/context-menu-trigger.ts b/src/cdk/menu/context-menu-trigger.ts index 4a86b9c02aec..9d56d1c877d9 100644 --- a/src/cdk/menu/context-menu-trigger.ts +++ b/src/cdk/menu/context-menu-trigger.ts @@ -75,7 +75,12 @@ export type ContextMenuCoordinates = {x: number; y: number}; {name: 'menuPosition', alias: 'cdkContextMenuPosition'}, {name: 'menuData', alias: 'cdkContextMenuTriggerData'}, ], - outputs: ['opened: cdkContextMenuOpened', 'closed: cdkContextMenuClosed'], + outputs: [ + 'opened: cdkContextMenuOpened', + 'closed: cdkContextMenuClosed', + 'outsideClicked: cdkContextMenuOutsideClicked', + 'triggered: cdkContextMenuTriggered', + ], providers: [ {provide: MENU_TRIGGER, useExisting: CdkContextMenuTrigger}, {provide: MENU_STACK, useClass: MenuStack}, @@ -96,6 +101,10 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr /** Whether the context menu is disabled. */ @Input({alias: 'cdkContextMenuDisabled', transform: booleanAttribute}) disabled: boolean = false; + /** Whether on clicking outside of menu should close it */ + @Input({alias: 'cdkContextMenuDisableCloseOnOutsideClick', transform: booleanAttribute}) + disableCloseOnOutsideClick: boolean = false; + constructor() { super(); this._setMenuStackCloseListener(); @@ -140,6 +149,9 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr } else { this.childMenu?.focusFirstItem('program'); } + + // Emit that the user have triggered contextmenu event. + this.triggered.emit({x: event.clientX, y: event.clientY}); } } @@ -210,7 +222,13 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr outsideClicks.pipe(takeUntil(this.stopOutsideClicksListener)).subscribe(event => { if (!this.isElementInsideMenuStack(_getEventTarget(event)!)) { - this.menuStack.closeAll(); + // We do not want to close menu if user does not want to on outside clicks. + if (!this.disableCloseOnOutsideClick) { + this.menuStack.closeAll(); + } + + // Emit that we had a click outside the menu. + this.outsideClicked.emit(event); } }); } diff --git a/src/cdk/menu/menu-trigger-base.ts b/src/cdk/menu/menu-trigger-base.ts index 840ca2f65a1d..a597d0fcbb09 100644 --- a/src/cdk/menu/menu-trigger-base.ts +++ b/src/cdk/menu/menu-trigger-base.ts @@ -73,6 +73,12 @@ export abstract class CdkMenuTriggerBase implements OnDestroy { /** Emits when the attached menu is requested to close */ readonly closed: EventEmitter = new EventEmitter(); + /** Emits when the attached menu has click outside of it in open state */ + readonly outsideClicked: EventEmitter = new EventEmitter(); + + /** Emits when the user triggers context menu */ + readonly triggered: EventEmitter<{x: number; y: number}> = new EventEmitter(); + /** Template reference variable to the menu this trigger opens */ menuTemplateRef: TemplateRef | null; diff --git a/tools/public_api_guard/cdk/menu.md b/tools/public_api_guard/cdk/menu.md index ffab9c7ed73d..fa321319ffc3 100644 --- a/tools/public_api_guard/cdk/menu.md +++ b/tools/public_api_guard/cdk/menu.md @@ -35,13 +35,16 @@ export const CDK_MENU: InjectionToken; export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestroy { constructor(); close(): void; + disableCloseOnOutsideClick: boolean; disabled: boolean; // (undocumented) + static ngAcceptInputType_disableCloseOnOutsideClick: unknown; + // (undocumented) static ngAcceptInputType_disabled: unknown; open(coordinates: ContextMenuCoordinates): void; _openOnContextMenu(event: MouseEvent): void; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -232,9 +235,14 @@ export abstract class CdkMenuTriggerBase implements OnDestroy { // (undocumented) ngOnDestroy(): void; readonly opened: EventEmitter; + readonly outsideClicked: EventEmitter; protected overlayRef: OverlayRef | null; registerChildMenu(child: Menu): void; protected readonly stopOutsideClicksListener: Observable; + readonly triggered: EventEmitter<{ + x: number; + y: number; + }>; protected readonly viewContainerRef: ViewContainerRef; // (undocumented) static ɵdir: i0.ɵɵDirectiveDeclaration;