Skip to content

Commit 3e9228a

Browse files
committed
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. (cherry picked from commit d336370)
1 parent 5cd7204 commit 3e9228a

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
@@ -17,7 +17,7 @@ import {
1717
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
1818
import {_getEventTarget} from '@angular/cdk/platform';
1919
import {merge, partition} from 'rxjs';
20-
import {skip, takeUntil} from 'rxjs/operators';
20+
import {skip, takeUntil, skipWhile} from 'rxjs/operators';
2121
import {MENU_STACK, MenuStack} from './menu-stack';
2222
import {CdkMenuTriggerBase, MENU_TRIGGER} from './menu-trigger-base';
2323

@@ -104,7 +104,7 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr
104104
* @param coordinates where to open the context menu
105105
*/
106106
open(coordinates: ContextMenuCoordinates) {
107-
this._open(coordinates, false);
107+
this._open(null, coordinates);
108108
}
109109

110110
/** Close the currently opened context menu. */
@@ -127,7 +127,7 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr
127127
event.stopPropagation();
128128

129129
this._contextMenuTracker.update(this);
130-
this._open({x: event.clientX, y: event.clientY}, true);
130+
this._open(event, {x: event.clientX, y: event.clientY});
131131

132132
// A context menu can be triggered via a mouse right click or a keyboard shortcut.
133133
if (event.button === 2) {
@@ -180,17 +180,31 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr
180180
/**
181181
* Subscribe to the overlays outside pointer events stream and handle closing out the stack if a
182182
* click occurs outside the menus.
183-
* @param ignoreFirstAuxClick Whether to ignore the first auxclick event outside the menu.
183+
* @param userEvent User-generated event that opened the menu.
184184
*/
185-
private _subscribeToOutsideClicks(ignoreFirstAuxClick: boolean) {
185+
private _subscribeToOutsideClicks(userEvent: MouseEvent | null) {
186186
if (this.overlayRef) {
187187
let outsideClicks = this.overlayRef.outsidePointerEvents();
188-
// If the menu was triggered by the `contextmenu` event, skip the first `auxclick` event
189-
// because it fires when the mouse is released on the same click that opened the menu.
190-
if (ignoreFirstAuxClick) {
188+
189+
if (userEvent) {
191190
const [auxClicks, nonAuxClicks] = partition(outsideClicks, ({type}) => type === 'auxclick');
192-
outsideClicks = merge(nonAuxClicks, auxClicks.pipe(skip(1)));
191+
outsideClicks = merge(
192+
// Using a mouse, the `contextmenu` event can fire either when pressing the right button
193+
// or left button + control. Most browsers won't dispatch a `click` event right after
194+
// a `contextmenu` event triggered by left button + control, but Safari will (see #27832).
195+
// This closes the menu immediately. To work around it, we check that both the triggering
196+
// event and the current outside click event both had the control key pressed, and that
197+
// that this is the first outside click event.
198+
nonAuxClicks.pipe(
199+
skipWhile((event, index) => userEvent.ctrlKey && index === 0 && event.ctrlKey),
200+
),
201+
202+
// If the menu was triggered by the `contextmenu` event, skip the first `auxclick` event
203+
// because it fires when the mouse is released on the same click that opened the menu.
204+
auxClicks.pipe(skip(1)),
205+
);
193206
}
207+
194208
outsideClicks.pipe(takeUntil(this.stopOutsideClicksListener)).subscribe(event => {
195209
if (!this.isElementInsideMenuStack(_getEventTarget(event)!)) {
196210
this.menuStack.closeAll();
@@ -201,10 +215,10 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr
201215

202216
/**
203217
* Open the attached menu at the specified location.
218+
* @param userEvent User-generated event that opened the menu
204219
* @param coordinates where to open the context menu
205-
* @param ignoreFirstOutsideAuxClick Whether to ignore the first auxclick outside the menu after opening.
206220
*/
207-
private _open(coordinates: ContextMenuCoordinates, ignoreFirstOutsideAuxClick: boolean) {
221+
private _open(userEvent: MouseEvent | null, coordinates: ContextMenuCoordinates) {
208222
if (this.disabled) {
209223
return;
210224
}
@@ -230,7 +244,7 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr
230244
}
231245

232246
this.overlayRef.attach(this.getMenuContentPortal());
233-
this._subscribeToOutsideClicks(ignoreFirstOutsideAuxClick);
247+
this._subscribeToOutsideClicks(userEvent);
234248
}
235249
}
236250
}

0 commit comments

Comments
 (0)