From f4e1ac8cafb75dfab129b95f0eb39591579f3d03 Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Wed, 19 Jun 2019 11:00:44 -0700 Subject: [PATCH] [Flare] Press events include defaultPrevented (#15916) * Rename `disableContextMenu` to `preventContextMenu` * Change the behaviour of `preventContextMenu` so that `onContextMenu` is still called when the native context menu is prevented. --- .../src/events/DOMEventResponderSystem.js | 8 --- .../DOMEventResponderSystem-test.internal.js | 13 ---- packages/react-events/docs/Press.md | 39 +++++++++-- packages/react-events/src/Press.js | 64 +++++++++++-------- .../src/__tests__/Press-test.internal.js | 21 +++++- 5 files changed, 89 insertions(+), 56 deletions(-) diff --git a/packages/react-dom/src/events/DOMEventResponderSystem.js b/packages/react-dom/src/events/DOMEventResponderSystem.js index ff6ab2737b..1e34b5079b 100644 --- a/packages/react-dom/src/events/DOMEventResponderSystem.js +++ b/packages/react-dom/src/events/DOMEventResponderSystem.js @@ -171,14 +171,6 @@ const eventResponderContext: ReactDOMResponderContext = { } }, }); - // $FlowFixMe: we don't need value, Flow thinks we do - Object.defineProperty(possibleEventObject, 'defaultPrevented', { - get() { - if (__DEV__) { - showWarning('defaultPrevented'); - } - }, - }); const eventObject = ((possibleEventObject: any): $Shape< PartialEventObject, diff --git a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js index 07f8828b53..1903f02b56 100644 --- a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js @@ -925,19 +925,6 @@ describe('DOMEventResponderSystem', () => { ' Try wrapping in a conditional, i.e. `if (event.type !== "press") { event.nativeEvent }`', {withoutStack: true}, ); - expect(() => { - handler = event => { - return event.defaultPrevented; - }; - ReactDOM.render(, container); - dispatchClickEvent(document.body); - }).toWarnDev( - 'Warning: defaultPrevented is not available on event objects created from event responder modules ' + - '(React Flare).' + - ' Try wrapping in a conditional, i.e. `if (event.type !== "press") { event.defaultPrevented }`', - {withoutStack: true}, - ); - expect(container.innerHTML).toBe(''); }); diff --git a/packages/react-events/docs/Press.md b/packages/react-events/docs/Press.md index 71d269b3c3..d618d7121c 100644 --- a/packages/react-events/docs/Press.md +++ b/packages/react-events/docs/Press.md @@ -37,9 +37,34 @@ const Button = (props) => ( ```js type PressEvent = { - pointerType: 'mouse' | 'touch' | 'pen' | 'trackpad' | 'keyboard', + altKey: boolean, + ctrlKey: boolean, + defaultPrevented: boolean, + metaKey: boolean, + pageX: number, + pageY: number, + pointerType: + | 'mouse' + | 'touch' + | 'pen' + | 'trackpad' + | 'keyboard', + screenX: number, + screenY: number, + shiftKey: boolean, target: Element, - type: 'press' | 'pressstart' | 'pressend' | 'presschange' | 'pressmove' | 'longpress' | 'longpresschange' | 'contextmenu' + timeStamp: number, + type: + | 'press' + | 'pressstart' + | 'pressend' + | 'presschange' + | 'pressmove' + | 'longpress' + | 'longpresschange' + | 'contextmenu', + x: number, + y: number } type PressOffset = { @@ -71,11 +96,6 @@ released before the threshold is exceeded. Disables all `Press` events. -### disableContextMenu: boolean = false - -Disables the native context menu so that it is never shown and `onContextMenu` -is never called. - ### onContextMenu: (e: PressEvent) => void Called when the context menu is shown. When a press is active, the context menu @@ -135,6 +155,11 @@ down) can be moved back within the bounds of the element to reactivate it. Ensure you pass in a constant to reduce memory allocations. Default is `20` for each offset. +### preventContextMenu: boolean = false + +Prevents the native context menu from being shown, but `onContextMenu` +is still called. + ### preventDefault: boolean = true Whether to `preventDefault()` native events. Native behavior is prevented by diff --git a/packages/react-events/src/Press.js b/packages/react-events/src/Press.js index 4af1a798a7..2a6f788aef 100644 --- a/packages/react-events/src/Press.js +++ b/packages/react-events/src/Press.js @@ -19,7 +19,6 @@ import {DiscreteEvent, UserBlockingEvent} from 'shared/ReactTypes'; type PressProps = { disabled: boolean, - disableContextMenu: boolean, delayLongPress: number, delayPressEnd: number, delayPressStart: number, @@ -38,6 +37,7 @@ type PressProps = { bottom: number, left: number, }, + preventContextMenu: boolean, preventDefault: boolean, stopPropagation: boolean, }; @@ -72,6 +72,7 @@ type PressState = { |}>, ignoreEmulatedMouseEvents: boolean, activePointerId: null | number, + shouldPreventClick: boolean, }; type PressEventType = @@ -85,6 +86,7 @@ type PressEventType = | 'contextmenu'; type PressEvent = {| + defaultPrevented: boolean, target: Element | Document, type: PressEventType, pointerType: PointerType, @@ -155,6 +157,7 @@ function createPressEvent( target: Element | Document, pointerType: PointerType, event: ?ReactDOMResponderEvent, + defaultPrevented: boolean, ): PressEvent { const timeStamp = context.getTimeStamp(); let clientX = null; @@ -184,6 +187,7 @@ function createPressEvent( } } return { + defaultPrevented, target, type, pointerType, @@ -213,12 +217,16 @@ function dispatchEvent( ): void { const target = ((state.pressTarget: any): Element | Document); const pointerType = state.pointerType; + const defaultPrevented = + (event != null && event.nativeEvent.defaultPrevented === true) || + (name === 'press' && state.shouldPreventClick); const syntheticEvent = createPressEvent( context, name, target, pointerType, event, + defaultPrevented, ); context.dispatchEvent(syntheticEvent, listener, eventPriority); } @@ -631,6 +639,7 @@ const PressResponder = { responderRegionOnDeactivation: null, ignoreEmulatedMouseEvents: false, activePointerId: null, + shouldPreventClick: false, }; }, allowMultipleHostChildren: false, @@ -728,14 +737,13 @@ const PressResponder = { } case 'contextmenu': { - if (props.disableContextMenu) { + if (props.preventContextMenu) { // Skip dispatching of onContextMenu below nativeEvent.preventDefault(); - return; } if (isPressed) { - if (props.preventDefault !== false) { + if (props.preventDefault !== false && !nativeEvent.defaultPrevented) { // Skip dispatching of onContextMenu below nativeEvent.preventDefault(); return; @@ -863,6 +871,31 @@ const PressResponder = { isKeyboardEvent = true; } + // Determine whether to call preventDefault on subsequent native events. + state.shouldPreventClick = false; + if ( + context.isTargetWithinEventComponent(target) && + context.isTargetWithinHostComponent(target, 'a', true) + ) { + const { + altKey, + ctrlKey, + metaKey, + shiftKey, + } = (nativeEvent: MouseEvent); + // Check "open in new window/tab" and "open context menu" key modifiers + const preventDefault = props.preventDefault; + if ( + preventDefault !== false && + !shiftKey && + !metaKey && + !ctrlKey && + !altKey + ) { + state.shouldPreventClick = true; + } + } + const wasLongPressed = state.isLongPressed; dispatchPressEndEvents(event, context, props, state); @@ -906,27 +939,8 @@ const PressResponder = { case 'click': { removeRootEventTypes(context, state); - if ( - context.isTargetWithinEventComponent(target) && - context.isTargetWithinHostComponent(target, 'a', true) - ) { - const { - altKey, - ctrlKey, - metaKey, - shiftKey, - } = (nativeEvent: MouseEvent); - // Check "open in new window/tab" and "open context menu" key modifiers - const preventDefault = props.preventDefault; - if ( - preventDefault !== false && - !shiftKey && - !metaKey && - !ctrlKey && - !altKey - ) { - nativeEvent.preventDefault(); - } + if (state.shouldPreventClick) { + nativeEvent.preventDefault(); } break; } diff --git a/packages/react-events/src/__tests__/Press-test.internal.js b/packages/react-events/src/__tests__/Press-test.internal.js index b2c49c2b6f..42510a2458 100644 --- a/packages/react-events/src/__tests__/Press-test.internal.js +++ b/packages/react-events/src/__tests__/Press-test.internal.js @@ -2221,6 +2221,9 @@ describe('Event responder: Press', () => { ref.current.dispatchEvent(createEvent('pointerup')); ref.current.dispatchEvent(createEvent('click', {preventDefault})); expect(preventDefault).toBeCalled(); + expect(onPress).toHaveBeenCalledWith( + expect.objectContaining({defaultPrevented: true}), + ); }); it('deeply prevents native behaviour by default', () => { @@ -2259,6 +2262,9 @@ describe('Event responder: Press', () => { ref.current.dispatchEvent(createEvent('pointerup')); ref.current.dispatchEvent(createEvent('click', {preventDefault})); expect(preventDefault).toBeCalled(); + expect(onPress).toHaveBeenCalledWith( + expect.objectContaining({defaultPrevented: true}), + ); }); it('uses native behaviour for interactions with modifier keys', () => { @@ -2283,6 +2289,9 @@ describe('Event responder: Press', () => { createEvent('click', {[modifierKey]: true, preventDefault}), ); expect(preventDefault).not.toBeCalled(); + expect(onPress).toHaveBeenCalledWith( + expect.objectContaining({defaultPrevented: false}), + ); }); }); @@ -2301,6 +2310,9 @@ describe('Event responder: Press', () => { ref.current.dispatchEvent(createEvent('pointerup')); ref.current.dispatchEvent(createEvent('click', {preventDefault})); expect(preventDefault).not.toBeCalled(); + expect(onPress).toHaveBeenCalledWith( + expect.objectContaining({defaultPrevented: false}), + ); }); }); @@ -2839,11 +2851,11 @@ describe('Event responder: Press', () => { expect(onContextMenu).toHaveBeenCalledTimes(0); }); - it('is not called if "disableContextMenu" is true', () => { + it('is still called if "preventContextMenu" is true', () => { const onContextMenu = jest.fn(); const ref = React.createRef(); const element = ( - +
); @@ -2852,7 +2864,10 @@ describe('Event responder: Press', () => { createEvent('pointerdown', {pointerType: 'mouse', button: 2}), ); ref.current.dispatchEvent(createEvent('contextmenu')); - expect(onContextMenu).toHaveBeenCalledTimes(0); + expect(onContextMenu).toHaveBeenCalledTimes(1); + expect(onContextMenu).toHaveBeenCalledWith( + expect.objectContaining({defaultPrevented: true}), + ); }); });