Skip to content

Commit d336370

Browse files
authored
fix(cdk/menu): context menu closing immediately on control + click on Safari (#27838)
Works around a browser inconsistency that was causing the context menu to close immediately when it is opened with control + left click on Safari. Fixes #27832.
1 parent 534b546 commit d336370

File tree

1 file changed

+26
-12
lines changed

1 file changed

+26
-12
lines changed

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

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from '@angular/cdk/overlay';
1717
import {_getEventTarget} from '@angular/cdk/platform';
1818
import {merge, partition} from 'rxjs';
19-
import {skip, takeUntil} from 'rxjs/operators';
19+
import {skip, takeUntil, skipWhile} from 'rxjs/operators';
2020
import {MENU_STACK, MenuStack} from './menu-stack';
2121
import {CdkMenuTriggerBase, MENU_TRIGGER} from './menu-trigger-base';
2222

@@ -96,7 +96,7 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr
9696
* @param coordinates where to open the context menu
9797
*/
9898
open(coordinates: ContextMenuCoordinates) {
99-
this._open(coordinates, false);
99+
this._open(null, coordinates);
100100
}
101101

102102
/** Close the currently opened context menu. */
@@ -119,7 +119,7 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr
119119
event.stopPropagation();
120120

121121
this._contextMenuTracker.update(this);
122-
this._open({x: event.clientX, y: event.clientY}, true);
122+
this._open(event, {x: event.clientX, y: event.clientY});
123123

124124
// A context menu can be triggered via a mouse right click or a keyboard shortcut.
125125
if (event.button === 2) {
@@ -172,17 +172,31 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr
172172
/**
173173
* Subscribe to the overlays outside pointer events stream and handle closing out the stack if a
174174
* click occurs outside the menus.
175-
* @param ignoreFirstAuxClick Whether to ignore the first auxclick event outside the menu.
175+
* @param userEvent User-generated event that opened the menu.
176176
*/
177-
private _subscribeToOutsideClicks(ignoreFirstAuxClick: boolean) {
177+
private _subscribeToOutsideClicks(userEvent: MouseEvent | null) {
178178
if (this.overlayRef) {
179179
let outsideClicks = this.overlayRef.outsidePointerEvents();
180-
// If the menu was triggered by the `contextmenu` event, skip the first `auxclick` event
181-
// because it fires when the mouse is released on the same click that opened the menu.
182-
if (ignoreFirstAuxClick) {
180+
181+
if (userEvent) {
183182
const [auxClicks, nonAuxClicks] = partition(outsideClicks, ({type}) => type === 'auxclick');
184-
outsideClicks = merge(nonAuxClicks, auxClicks.pipe(skip(1)));
183+
outsideClicks = merge(
184+
// Using a mouse, the `contextmenu` event can fire either when pressing the right button
185+
// or left button + control. Most browsers won't dispatch a `click` event right after
186+
// a `contextmenu` event triggered by left button + control, but Safari will (see #27832).
187+
// This closes the menu immediately. To work around it, we check that both the triggering
188+
// event and the current outside click event both had the control key pressed, and that
189+
// that this is the first outside click event.
190+
nonAuxClicks.pipe(
191+
skipWhile((event, index) => userEvent.ctrlKey && index === 0 && event.ctrlKey),
192+
),
193+
194+
// If the menu was triggered by the `contextmenu` event, skip the first `auxclick` event
195+
// because it fires when the mouse is released on the same click that opened the menu.
196+
auxClicks.pipe(skip(1)),
197+
);
185198
}
199+
186200
outsideClicks.pipe(takeUntil(this.stopOutsideClicksListener)).subscribe(event => {
187201
if (!this.isElementInsideMenuStack(_getEventTarget(event)!)) {
188202
this.menuStack.closeAll();
@@ -193,10 +207,10 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr
193207

194208
/**
195209
* Open the attached menu at the specified location.
210+
* @param userEvent User-generated event that opened the menu
196211
* @param coordinates where to open the context menu
197-
* @param ignoreFirstOutsideAuxClick Whether to ignore the first auxclick outside the menu after opening.
198212
*/
199-
private _open(coordinates: ContextMenuCoordinates, ignoreFirstOutsideAuxClick: boolean) {
213+
private _open(userEvent: MouseEvent | null, coordinates: ContextMenuCoordinates) {
200214
if (this.disabled) {
201215
return;
202216
}
@@ -222,7 +236,7 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr
222236
}
223237

224238
this.overlayRef.attach(this.getMenuContentPortal());
225-
this._subscribeToOutsideClicks(ignoreFirstOutsideAuxClick);
239+
this._subscribeToOutsideClicks(userEvent);
226240
}
227241
}
228242
}

0 commit comments

Comments
 (0)