Skip to content

Commit 75cda48

Browse files
authored
fix(cdk-experimental/menu): keep context menus open when mouse is released (#24308)
1 parent a948ec4 commit 75cda48

File tree

2 files changed

+57
-25
lines changed

2 files changed

+57
-25
lines changed

src/cdk-experimental/menu/context-menu.spec.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ describe('CdkContextMenuTrigger', () => {
3939

4040
/** Get the context in which the context menu should trigger. */
4141
function getMenuContext() {
42-
return fixture.componentInstance.trigger.nativeElement;
42+
return fixture.componentInstance.triggerElement.nativeElement;
4343
}
4444

4545
/** Open up the context menu and run change detection. */
@@ -73,6 +73,29 @@ describe('CdkContextMenuTrigger', () => {
7373
expect(getContextMenu()).not.toBeDefined();
7474
});
7575

76+
it('should not close the menu on first auxclick after opening via contextmenu event', () => {
77+
openContextMenu();
78+
79+
fixture.nativeElement.querySelector('#other').dispatchEvent(new MouseEvent('auxclick'));
80+
fixture.detectChanges();
81+
82+
expect(getContextMenu()).toBeDefined();
83+
84+
fixture.nativeElement.querySelector('#other').dispatchEvent(new MouseEvent('auxclick'));
85+
fixture.detectChanges();
86+
87+
expect(getContextMenu()).not.toBeDefined();
88+
});
89+
90+
it('should close the menu on first auxclick after opening programmatically', () => {
91+
fixture.componentInstance.trigger.open({x: 0, y: 0});
92+
93+
fixture.nativeElement.querySelector('#other').dispatchEvent(new MouseEvent('auxclick'));
94+
fixture.detectChanges();
95+
96+
expect(getContextMenu()).not.toBeDefined();
97+
});
98+
7699
it('should close out the context menu when clicking a menu item', () => {
77100
openContextMenu();
78101

@@ -397,7 +420,8 @@ describe('CdkContextMenuTrigger', () => {
397420
`,
398421
})
399422
class SimpleContextMenu {
400-
@ViewChild(CdkContextMenuTrigger, {read: ElementRef}) trigger: ElementRef<HTMLElement>;
423+
@ViewChild(CdkContextMenuTrigger) trigger: CdkContextMenuTrigger;
424+
@ViewChild(CdkContextMenuTrigger, {read: ElementRef}) triggerElement: ElementRef<HTMLElement>;
401425
@ViewChild(CdkMenu) menu?: CdkMenu;
402426
@ViewChild(CdkMenu, {read: ElementRef}) nativeMenu?: ElementRef<HTMLElement>;
403427

src/cdk-experimental/menu/context-menu.ts

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,28 @@
88

99
import {
1010
Directive,
11-
Input,
12-
ViewContainerRef,
13-
Output,
1411
EventEmitter,
15-
Optional,
16-
OnDestroy,
1712
Inject,
1813
Injectable,
1914
InjectionToken,
15+
Input,
16+
OnDestroy,
17+
Optional,
18+
Output,
19+
ViewContainerRef,
2020
} from '@angular/core';
2121
import {Directionality} from '@angular/cdk/bidi';
2222
import {
23-
OverlayRef,
23+
ConnectedPosition,
24+
FlexibleConnectedPositionStrategy,
2425
Overlay,
2526
OverlayConfig,
26-
FlexibleConnectedPositionStrategy,
27-
ConnectedPosition,
27+
OverlayRef,
2828
} from '@angular/cdk/overlay';
29-
import {TemplatePortal, Portal} from '@angular/cdk/portal';
30-
import {coerceBooleanProperty, BooleanInput} from '@angular/cdk/coercion';
31-
import {Subject, merge} from 'rxjs';
32-
import {takeUntil} from 'rxjs/operators';
29+
import {Portal, TemplatePortal} from '@angular/cdk/portal';
30+
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
31+
import {merge, partition, Subject} from 'rxjs';
32+
import {skip, takeUntil} from 'rxjs/operators';
3333
import {CdkMenuPanel} from './menu-panel';
3434
import {MenuStack} from './menu-stack';
3535
import {throwExistingMenuStackError} from './menu-errors';
@@ -152,6 +152,10 @@ export class CdkContextMenuTrigger implements OnDestroy {
152152
* @param coordinates where to open the context menu
153153
*/
154154
open(coordinates: ContextMenuCoordinates) {
155+
this._open(coordinates, false);
156+
}
157+
158+
private _open(coordinates: ContextMenuCoordinates, ignoreFirstOutsideAuxClick: boolean) {
155159
if (this.disabled) {
156160
return;
157161
} else if (this.isOpen()) {
@@ -176,7 +180,7 @@ export class CdkContextMenuTrigger implements OnDestroy {
176180
}
177181

178182
this._overlayRef.attach(this._getMenuContent());
179-
this._subscribeToOutsideClicks();
183+
this._subscribeToOutsideClicks(ignoreFirstOutsideAuxClick);
180184
}
181185
}
182186

@@ -200,7 +204,7 @@ export class CdkContextMenuTrigger implements OnDestroy {
200204
event.stopPropagation();
201205

202206
this._contextMenuTracker.update(this);
203-
this.open({x: event.clientX, y: event.clientY});
207+
this._open({x: event.clientX, y: event.clientY}, true);
204208

205209
// A context menu can be triggered via a mouse right click or a keyboard shortcut.
206210
if (event.button === 2) {
@@ -285,16 +289,20 @@ export class CdkContextMenuTrigger implements OnDestroy {
285289
* Subscribe to the overlays outside pointer events stream and handle closing out the stack if a
286290
* click occurs outside the menus.
287291
*/
288-
private _subscribeToOutsideClicks() {
292+
private _subscribeToOutsideClicks(ignoreFirstAuxClick: boolean) {
289293
if (this._overlayRef) {
290-
this._overlayRef
291-
.outsidePointerEvents()
292-
.pipe(takeUntil(this._stopOutsideClicksListener))
293-
.subscribe(event => {
294-
if (!isClickInsideMenuOverlay(event.target as Element)) {
295-
this._menuStack.closeAll();
296-
}
297-
});
294+
let outsideClicks = this._overlayRef.outsidePointerEvents();
295+
// If the menu was triggered by the `contextmenu` event, skip the first `auxclick` event
296+
// because it fires when the mouse is released on the same click that opened the menu.
297+
if (ignoreFirstAuxClick) {
298+
const [auxClicks, nonAuxClicks] = partition(outsideClicks, ({type}) => type === 'auxclick');
299+
outsideClicks = merge(nonAuxClicks, auxClicks.pipe(skip(1)));
300+
}
301+
outsideClicks.pipe(takeUntil(this._stopOutsideClicksListener)).subscribe(event => {
302+
if (!isClickInsideMenuOverlay(event.target as Element)) {
303+
this._menuStack.closeAll();
304+
}
305+
});
298306
}
299307
}
300308

0 commit comments

Comments
 (0)