Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(cdk/menu): context menu closing immediately on control + click on Safari #27838

Merged
merged 1 commit into from
Sep 25, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 26 additions & 12 deletions src/cdk/menu/context-menu-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from '@angular/cdk/overlay';
import {_getEventTarget} from '@angular/cdk/platform';
import {merge, partition} from 'rxjs';
import {skip, takeUntil} from 'rxjs/operators';
import {skip, takeUntil, skipWhile} from 'rxjs/operators';
import {MENU_STACK, MenuStack} from './menu-stack';
import {CdkMenuTriggerBase, MENU_TRIGGER} from './menu-trigger-base';

Expand Down Expand Up @@ -96,7 +96,7 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr
* @param coordinates where to open the context menu
*/
open(coordinates: ContextMenuCoordinates) {
this._open(coordinates, false);
this._open(null, coordinates);
}

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

this._contextMenuTracker.update(this);
this._open({x: event.clientX, y: event.clientY}, true);
this._open(event, {x: event.clientX, y: event.clientY});

// A context menu can be triggered via a mouse right click or a keyboard shortcut.
if (event.button === 2) {
Expand Down Expand Up @@ -172,17 +172,31 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr
/**
* Subscribe to the overlays outside pointer events stream and handle closing out the stack if a
* click occurs outside the menus.
* @param ignoreFirstAuxClick Whether to ignore the first auxclick event outside the menu.
* @param userEvent User-generated event that opened the menu.
*/
private _subscribeToOutsideClicks(ignoreFirstAuxClick: boolean) {
private _subscribeToOutsideClicks(userEvent: MouseEvent | null) {
if (this.overlayRef) {
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) {

if (userEvent) {
const [auxClicks, nonAuxClicks] = partition(outsideClicks, ({type}) => type === 'auxclick');
outsideClicks = merge(nonAuxClicks, auxClicks.pipe(skip(1)));
outsideClicks = merge(
// Using a mouse, the `contextmenu` event can fire either when pressing the right button
// or left button + control. Most browsers won't dispatch a `click` event right after
// a `contextmenu` event triggered by left button + control, but Safari will (see #27832).
// This closes the menu immediately. To work around it, we check that both the triggering
// event and the current outside click event both had the control key pressed, and that
// that this is the first outside click event.
nonAuxClicks.pipe(
skipWhile((event, index) => userEvent.ctrlKey && index === 0 && event.ctrlKey),
),

// 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.
auxClicks.pipe(skip(1)),
);
}

outsideClicks.pipe(takeUntil(this.stopOutsideClicksListener)).subscribe(event => {
if (!this.isElementInsideMenuStack(_getEventTarget(event)!)) {
this.menuStack.closeAll();
Expand All @@ -193,10 +207,10 @@ export class CdkContextMenuTrigger extends CdkMenuTriggerBase implements OnDestr

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

this.overlayRef.attach(this.getMenuContentPortal());
this._subscribeToOutsideClicks(ignoreFirstOutsideAuxClick);
this._subscribeToOutsideClicks(userEvent);
}
}
}
Loading