From 7aff50a6d83748fa4d4aff7028dc80aa9ba25441 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 1 Feb 2022 22:57:08 +0000 Subject: [PATCH] fix(cdk-experimental/menu): keep context menus open when mouse is released (#24308) (cherry picked from commit 75cda48da100335f875ad6ba87db25ab01d0e972) --- .../menu/context-menu.spec.ts | 28 +++++++++- src/cdk-experimental/menu/context-menu.ts | 54 +++++++++++-------- 2 files changed, 57 insertions(+), 25 deletions(-) diff --git a/src/cdk-experimental/menu/context-menu.spec.ts b/src/cdk-experimental/menu/context-menu.spec.ts index f5a2d6abd753..dbf4324bbc9c 100644 --- a/src/cdk-experimental/menu/context-menu.spec.ts +++ b/src/cdk-experimental/menu/context-menu.spec.ts @@ -39,7 +39,7 @@ describe('CdkContextMenuTrigger', () => { /** Get the context in which the context menu should trigger. */ function getMenuContext() { - return fixture.componentInstance.trigger.nativeElement; + return fixture.componentInstance.triggerElement.nativeElement; } /** Open up the context menu and run change detection. */ @@ -73,6 +73,29 @@ describe('CdkContextMenuTrigger', () => { expect(getContextMenu()).not.toBeDefined(); }); + it('should not close the menu on first auxclick after opening via contextmenu event', () => { + openContextMenu(); + + fixture.nativeElement.querySelector('#other').dispatchEvent(new MouseEvent('auxclick')); + fixture.detectChanges(); + + expect(getContextMenu()).toBeDefined(); + + fixture.nativeElement.querySelector('#other').dispatchEvent(new MouseEvent('auxclick')); + fixture.detectChanges(); + + expect(getContextMenu()).not.toBeDefined(); + }); + + it('should close the menu on first auxclick after opening programmatically', () => { + fixture.componentInstance.trigger.open({x: 0, y: 0}); + + fixture.nativeElement.querySelector('#other').dispatchEvent(new MouseEvent('auxclick')); + fixture.detectChanges(); + + expect(getContextMenu()).not.toBeDefined(); + }); + it('should close out the context menu when clicking a menu item', () => { openContextMenu(); @@ -397,7 +420,8 @@ describe('CdkContextMenuTrigger', () => { `, }) class SimpleContextMenu { - @ViewChild(CdkContextMenuTrigger, {read: ElementRef}) trigger: ElementRef; + @ViewChild(CdkContextMenuTrigger) trigger: CdkContextMenuTrigger; + @ViewChild(CdkContextMenuTrigger, {read: ElementRef}) triggerElement: ElementRef; @ViewChild(CdkMenu) menu?: CdkMenu; @ViewChild(CdkMenu, {read: ElementRef}) nativeMenu?: ElementRef; diff --git a/src/cdk-experimental/menu/context-menu.ts b/src/cdk-experimental/menu/context-menu.ts index cb291ad6b6d9..481add83506d 100644 --- a/src/cdk-experimental/menu/context-menu.ts +++ b/src/cdk-experimental/menu/context-menu.ts @@ -8,28 +8,28 @@ import { Directive, - Input, - ViewContainerRef, - Output, EventEmitter, - Optional, - OnDestroy, Inject, Injectable, InjectionToken, + Input, + OnDestroy, + Optional, + Output, + ViewContainerRef, } from '@angular/core'; import {Directionality} from '@angular/cdk/bidi'; import { - OverlayRef, + ConnectedPosition, + FlexibleConnectedPositionStrategy, Overlay, OverlayConfig, - FlexibleConnectedPositionStrategy, - ConnectedPosition, + OverlayRef, } from '@angular/cdk/overlay'; -import {TemplatePortal, Portal} from '@angular/cdk/portal'; -import {coerceBooleanProperty, BooleanInput} from '@angular/cdk/coercion'; -import {Subject, merge} from 'rxjs'; -import {takeUntil} from 'rxjs/operators'; +import {Portal, TemplatePortal} from '@angular/cdk/portal'; +import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; +import {merge, partition, Subject} from 'rxjs'; +import {skip, takeUntil} from 'rxjs/operators'; import {CdkMenuPanel} from './menu-panel'; import {MenuStack} from './menu-stack'; import {throwExistingMenuStackError} from './menu-errors'; @@ -152,6 +152,10 @@ export class CdkContextMenuTrigger implements OnDestroy { * @param coordinates where to open the context menu */ open(coordinates: ContextMenuCoordinates) { + this._open(coordinates, false); + } + + private _open(coordinates: ContextMenuCoordinates, ignoreFirstOutsideAuxClick: boolean) { if (this.disabled) { return; } else if (this.isOpen()) { @@ -176,7 +180,7 @@ export class CdkContextMenuTrigger implements OnDestroy { } this._overlayRef.attach(this._getMenuContent()); - this._subscribeToOutsideClicks(); + this._subscribeToOutsideClicks(ignoreFirstOutsideAuxClick); } } @@ -200,7 +204,7 @@ export class CdkContextMenuTrigger implements OnDestroy { event.stopPropagation(); this._contextMenuTracker.update(this); - this.open({x: event.clientX, y: event.clientY}); + this._open({x: event.clientX, y: event.clientY}, true); // A context menu can be triggered via a mouse right click or a keyboard shortcut. if (event.button === 2) { @@ -285,16 +289,20 @@ export class CdkContextMenuTrigger implements OnDestroy { * Subscribe to the overlays outside pointer events stream and handle closing out the stack if a * click occurs outside the menus. */ - private _subscribeToOutsideClicks() { + private _subscribeToOutsideClicks(ignoreFirstAuxClick: boolean) { if (this._overlayRef) { - this._overlayRef - .outsidePointerEvents() - .pipe(takeUntil(this._stopOutsideClicksListener)) - .subscribe(event => { - if (!isClickInsideMenuOverlay(event.target as Element)) { - this._menuStack.closeAll(); - } - }); + let outsideClicks = this._overlayRef.outsidePointerEvents(); + // If the menu was triggered by the `contextmenu` event, skip the first `auxclick` event + // because it fires when the mouse is released on the same click that opened the menu. + if (ignoreFirstAuxClick) { + const [auxClicks, nonAuxClicks] = partition(outsideClicks, ({type}) => type === 'auxclick'); + outsideClicks = merge(nonAuxClicks, auxClicks.pipe(skip(1))); + } + outsideClicks.pipe(takeUntil(this._stopOutsideClicksListener)).subscribe(event => { + if (!isClickInsideMenuOverlay(event.target as Element)) { + this._menuStack.closeAll(); + } + }); } }