diff --git a/packages/react-events/src/Press.js b/packages/react-events/src/Press.js
index d9c08d783c..17a03cb1ce 100644
--- a/packages/react-events/src/Press.js
+++ b/packages/react-events/src/Press.js
@@ -20,6 +20,7 @@ type PressProps = {
delayLongPress: number,
delayPressEnd: number,
delayPressStart: number,
+ onContextMenu: (e: PressEvent) => void,
onLongPress: (e: PressEvent) => void,
onLongPressChange: boolean => void,
onLongPressShouldCancelPress: () => boolean,
@@ -78,7 +79,8 @@ type PressEventType =
| 'pressend'
| 'presschange'
| 'longpress'
- | 'longpresschange';
+ | 'longpresschange'
+ | 'contextmenu';
type PressEvent = {|
target: Element | Document,
@@ -99,6 +101,7 @@ type PressEvent = {|
shiftKey: boolean,
|};
+const isMac = /^Mac/.test(navigator.platform);
const DEFAULT_PRESS_END_DELAY_MS = 0;
const DEFAULT_PRESS_START_DELAY_MS = 0;
const DEFAULT_LONG_PRESS_DELAY_MS = 500;
@@ -400,17 +403,10 @@ function dispatchCancel(
props: PressProps,
state: PressState,
): void {
- const nativeEvent: any = event.nativeEvent;
- const type = event.type;
-
if (state.isPressed) {
- if (type === 'contextmenu' && props.preventDefault !== false) {
- nativeEvent.preventDefault();
- } else {
- state.ignoreEmulatedMouseEvents = false;
- removeRootEventTypes(context, state);
- dispatchPressEndEvents(event, context, props, state);
- }
+ state.ignoreEmulatedMouseEvents = false;
+ removeRootEventTypes(context, state);
+ dispatchPressEndEvents(event, context, props, state);
} else if (state.allowPressReentry) {
removeRootEventTypes(context, state);
}
@@ -683,8 +679,9 @@ const PressResponder = {
return;
}
// Ignore mouse/pen pressing on touch hit target area
+ const isMouseType = pointerType === 'mouse';
if (
- (pointerType === 'mouse' || pointerType === 'pen') &&
+ (isMouseType || pointerType === 'pen') &&
isEventPositionWithinTouchHitTarget(event, context)
) {
// We need to prevent the native event to block the focus
@@ -692,14 +689,22 @@ const PressResponder = {
return;
}
- // Ignore any device buttons except left-mouse and touch/pen contact
- if (nativeEvent.button > 0) {
+ // We set these here, before the button check so we have this
+ // data around for handling of the context menu
+ state.pointerType = pointerType;
+ state.pressTarget = context.getEventCurrentTarget(event);
+
+ // Ignore any device buttons except left-mouse and touch/pen contact.
+ // Additionally we ignore left-mouse + ctrl-key with Macs as that
+ // acts like right-click and opens the contextmenu.
+ if (
+ nativeEvent.button > 0 ||
+ (isMac && isMouseType && nativeEvent.ctrlKey)
+ ) {
return;
}
state.allowPressReentry = true;
- state.pointerType = pointerType;
- state.pressTarget = context.getEventCurrentTarget(event);
state.responderRegionOnActivation = calculateResponderRegion(
context,
state.pressTarget,
@@ -717,9 +722,25 @@ const PressResponder = {
break;
}
- // CANCEL
case 'contextmenu': {
- dispatchCancel(event, context, props, state);
+ if (state.isPressed) {
+ dispatchCancel(event, context, props, state);
+ if (props.preventDefault !== false) {
+ // Skip dispatching of onContextMenu below
+ nativeEvent.preventDefault();
+ return;
+ }
+ }
+ if (props.onContextMenu) {
+ dispatchEvent(
+ event,
+ context,
+ state,
+ 'contextmenu',
+ props.onContextMenu,
+ true,
+ );
+ }
break;
}
diff --git a/packages/react-events/src/__tests__/Press-test.internal.js b/packages/react-events/src/__tests__/Press-test.internal.js
index 196b6c27d1..16e5292c36 100644
--- a/packages/react-events/src/__tests__/Press-test.internal.js
+++ b/packages/react-events/src/__tests__/Press-test.internal.js
@@ -36,18 +36,21 @@ const createKeyboardEvent = (type, data) => {
});
};
+function init() {
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
+ ReactFeatureFlags.enableEventAPI = true;
+ React = require('react');
+ ReactDOM = require('react-dom');
+ Press = require('react-events/press');
+ Scheduler = require('scheduler');
+}
+
describe('Event responder: Press', () => {
let container;
beforeEach(() => {
jest.resetModules();
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
- ReactFeatureFlags.enableEventAPI = true;
- React = require('react');
- ReactDOM = require('react-dom');
- Press = require('react-events/press');
- Scheduler = require('scheduler');
-
+ init();
container = document.createElement('div');
document.body.appendChild(container);
});
@@ -2579,4 +2582,100 @@ describe('Event responder: Press', () => {
Scheduler.flushAll();
document.body.removeChild(newContainer);
});
+
+ describe('onContextMenu', () => {
+ it('is called after a right mouse click', () => {
+ const onContextMenu = jest.fn();
+ const ref = React.createRef();
+ const element = (
+
+
+
+ );
+ ReactDOM.render(element, container);
+
+ ref.current.dispatchEvent(
+ createEvent('pointerdown', {pointerType: 'mouse', button: 2}),
+ );
+ ref.current.dispatchEvent(createEvent('contextmenu'));
+ expect(onContextMenu).toHaveBeenCalledTimes(1);
+ expect(onContextMenu).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType: 'mouse', type: 'contextmenu'}),
+ );
+ });
+
+ it('is called after a left mouse click + ctrl key on Mac', () => {
+ jest.resetModules();
+ const platformGetter = jest.spyOn(global.navigator, 'platform', 'get');
+ platformGetter.mockReturnValue('MacIntel');
+ init();
+
+ const onContextMenu = jest.fn();
+ const ref = React.createRef();
+ const element = (
+
+
+
+ );
+ ReactDOM.render(element, container);
+
+ ref.current.dispatchEvent(
+ createEvent('pointerdown', {
+ pointerType: 'mouse',
+ button: 0,
+ ctrlKey: true,
+ }),
+ );
+ ref.current.dispatchEvent(createEvent('contextmenu'));
+ expect(onContextMenu).toHaveBeenCalledTimes(1);
+ expect(onContextMenu).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType: 'mouse', type: 'contextmenu'}),
+ );
+ platformGetter.mockClear();
+ });
+
+ it('is not called after a left mouse click + ctrl key on Windows', () => {
+ jest.resetModules();
+ const platformGetter = jest.spyOn(global.navigator, 'platform', 'get');
+ platformGetter.mockReturnValue('Win32');
+ init();
+
+ const onContextMenu = jest.fn();
+ const ref = React.createRef();
+ const element = (
+
+
+
+ );
+ ReactDOM.render(element, container);
+
+ ref.current.dispatchEvent(
+ createEvent('pointerdown', {
+ pointerType: 'mouse',
+ button: 0,
+ ctrlKey: true,
+ }),
+ );
+ ref.current.dispatchEvent(createEvent('contextmenu'));
+ expect(onContextMenu).toHaveBeenCalledTimes(0);
+ platformGetter.mockClear();
+ });
+
+ it('is not called after a right mouse click occurs during an active press', () => {
+ const onContextMenu = jest.fn();
+ const ref = React.createRef();
+ const element = (
+
+
+
+ );
+ ReactDOM.render(element, container);
+
+ ref.current.dispatchEvent(
+ createEvent('pointerdown', {pointerType: 'mouse', button: 0}),
+ );
+ ref.current.dispatchEvent(createEvent('contextmenu'));
+ expect(onContextMenu).toHaveBeenCalledTimes(0);
+ });
+ });
});