From 142cf56cbfe6d470f57e675e4e7079e904832cdd Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 29 May 2019 17:53:55 +0100 Subject: [PATCH] [Flare] Adds onContextMenu and fixes some contextmenu related issues (#15761) --- packages/react-events/src/Press.js | 57 ++++++--- .../src/__tests__/Press-test.internal.js | 113 ++++++++++++++++-- 2 files changed, 145 insertions(+), 25 deletions(-) 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); + }); + }); });