From 824e9bec7a3f5aa290e5f4e7d7ad52261a7c5d40 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 26 Jun 2019 22:25:49 +0100 Subject: [PATCH] [Flare] Fix issues with touch + pointer interactions (#15997) --- packages/react-events/src/dom/Press.js | 163 ++++++++---------- .../src/dom/__tests__/Press-test.internal.js | 58 ++++++- 2 files changed, 126 insertions(+), 95 deletions(-) diff --git a/packages/react-events/src/dom/Press.js b/packages/react-events/src/dom/Press.js index f97b71e7a5..2f08a4a4eb 100644 --- a/packages/react-events/src/dom/Press.js +++ b/packages/react-events/src/dom/Press.js @@ -75,6 +75,7 @@ type PressState = { ignoreEmulatedMouseEvents: boolean, activePointerId: null | number, shouldPreventClick: boolean, + touchEvent: null | Touch, }; type PressEventType = @@ -160,6 +161,7 @@ function createPressEvent( target: Element | Document, pointerType: PointerType, event: ?ReactDOMResponderEvent, + touchEvent: null | Touch, defaultPrevented: boolean, ): PressEvent { const timeStamp = context.getTimeStamp(); @@ -180,11 +182,7 @@ function createPressEvent( // Only check for one property, checking for all of them is costly. We can assume // if clientX exists, so do the rest. let eventObject; - if (nativeEvent.clientX !== undefined) { - eventObject = (nativeEvent: any); - } else if (isNativeTouchEvent(nativeEvent)) { - eventObject = getTouchFromPressEvent(nativeEvent); - } + eventObject = (touchEvent: any) || (nativeEvent: any); if (eventObject) { ({clientX, clientY, pageX, pageY, screenX, screenY} = eventObject); } @@ -223,12 +221,14 @@ function dispatchEvent( const defaultPrevented = (event != null && event.nativeEvent.defaultPrevented === true) || (name === 'press' && state.shouldPreventClick); + const touchEvent = state.touchEvent; const syntheticEvent = createPressEvent( context, name, target, pointerType, event, + touchEvent, defaultPrevented, ); context.dispatchEvent(syntheticEvent, listener, eventPriority); @@ -269,10 +269,10 @@ function dispatchLongPressChangeEvent( function activate(event: ReactDOMResponderEvent, context, props, state) { const nativeEvent: any = event.nativeEvent; - const {x, y} = getEventViewportCoords(nativeEvent); + const {clientX: x, clientY: y} = state.touchEvent || nativeEvent; const wasActivePressed = state.isActivePressed; state.isActivePressed = true; - if (x !== null && y !== null) { + if (x !== undefined && y !== undefined) { state.activationPosition = {x, y}; } @@ -433,6 +433,7 @@ function dispatchCancel( props: PressProps, state: PressState, ): void { + state.touchEvent = null; if (state.isPressed) { state.ignoreEmulatedMouseEvents = false; dispatchPressEndEvents(event, context, props, state); @@ -495,33 +496,12 @@ function calculateResponderRegion( }; } -function isNativeTouchEvent(nativeEvent: Event): boolean { - const changedTouches = ((nativeEvent: any): TouchEvent).changedTouches; - return changedTouches && typeof changedTouches.length === 'number'; -} - -function getTouchFromPressEvent(nativeEvent: TouchEvent): Touch { - const {changedTouches, touches} = nativeEvent; - return changedTouches.length > 0 - ? changedTouches[0] - : touches.length > 0 - ? touches[0] - : (nativeEvent: any); -} - -function getEventViewportCoords( - nativeEvent: Event, -): {x: null | number, y: null | number} { - let eventObject = (nativeEvent: any); - if (isNativeTouchEvent(eventObject)) { - eventObject = getTouchFromPressEvent(eventObject); +function getTouchFromPressEvent(nativeEvent: TouchEvent): null | Touch { + const targetTouches = nativeEvent.targetTouches; + if (targetTouches.length > 0) { + return targetTouches[0]; } - const x = eventObject.clientX; - const y = eventObject.clientY; - return { - x: x != null ? x : null, - y: y != null ? y : null, - }; + return null; } function unmountResponder( @@ -575,54 +555,46 @@ function getTouchTarget(context: ReactDOMResponderContext, touchEvent: Touch) { } function updateIsPressWithinResponderRegion( - target: Element | Document, nativeEventOrTouchEvent: Event | Touch, context: ReactDOMResponderContext, props: PressProps, state: PressState, ): void { - let isPressWithinResponderRegion = true; - if ( - state.pressTarget != null && - !context.isTargetWithinElement(target, state.pressTarget) - ) { - // Calculate the responder region we use for deactivation if not - // already done during move event. - if (state.responderRegionOnDeactivation == null) { - state.responderRegionOnDeactivation = calculateResponderRegion( - context, - state.pressTarget, - props, - ); - } - const {responderRegionOnActivation, responderRegionOnDeactivation} = state; - let left, top, right, bottom; - - if (responderRegionOnActivation != null) { - left = responderRegionOnActivation.left; - top = responderRegionOnActivation.top; - right = responderRegionOnActivation.right; - bottom = responderRegionOnActivation.bottom; - - if (responderRegionOnDeactivation != null) { - left = Math.min(left, responderRegionOnDeactivation.left); - top = Math.min(top, responderRegionOnDeactivation.top); - right = Math.max(right, responderRegionOnDeactivation.right); - bottom = Math.max(bottom, responderRegionOnDeactivation.bottom); - } - } - const {clientX: x, clientY: y} = (nativeEventOrTouchEvent: any); - - isPressWithinResponderRegion = - left != null && - right != null && - top != null && - bottom != null && - x !== null && - y !== null && - (x >= left && x <= right && y >= top && y <= bottom); + // Calculate the responder region we use for deactivation if not + // already done during move event. + if (state.responderRegionOnDeactivation == null) { + state.responderRegionOnDeactivation = calculateResponderRegion( + context, + ((state.pressTarget: any): Element), + props, + ); } - state.isPressWithinResponderRegion = isPressWithinResponderRegion; + const {responderRegionOnActivation, responderRegionOnDeactivation} = state; + let left, top, right, bottom; + + if (responderRegionOnActivation != null) { + left = responderRegionOnActivation.left; + top = responderRegionOnActivation.top; + right = responderRegionOnActivation.right; + bottom = responderRegionOnActivation.bottom; + + if (responderRegionOnDeactivation != null) { + left = Math.min(left, responderRegionOnDeactivation.left); + top = Math.min(top, responderRegionOnDeactivation.top); + right = Math.max(right, responderRegionOnDeactivation.right); + bottom = Math.max(bottom, responderRegionOnDeactivation.bottom); + } + } + const {clientX: x, clientY: y} = (nativeEventOrTouchEvent: any); + + state.isPressWithinResponderRegion = + left != null && + right != null && + top != null && + bottom != null && + x !== null && + y !== null && + (x >= left && x <= right && y >= top && y <= bottom); } function handleStopPropagation( @@ -650,7 +622,6 @@ const PressResponder: ReactDOMEventResponder = { return { activationPosition: null, addedRootEvents: false, - didDispatchEvent: false, isActivePressed: false, isActivePressStart: false, isLongPressed: false, @@ -666,6 +637,7 @@ const PressResponder: ReactDOMEventResponder = { ignoreEmulatedMouseEvents: false, activePointerId: null, shouldPreventClick: false, + touchEvent: null, }; }, allowMultipleHostChildren: false, @@ -731,6 +703,10 @@ const PressResponder: ReactDOMEventResponder = { state.activePointerId = pointerId; } else if (isTouchEvent) { const touchEvent = getTouchFromPressEvent(nativeEvent); + if (touchEvent === null) { + return; + } + state.touchEvent = touchEvent; state.activePointerId = touchEvent.identifier; } @@ -852,18 +828,23 @@ const PressResponder: ReactDOMEventResponder = { if (touchEvent === null) { return; } - target = getTouchTarget(context, touchEvent); + state.touchEvent = touchEvent; } - // Calculate the responder region we use for deactivation, as the - // element dimensions may have changed since activation. - updateIsPressWithinResponderRegion( - target, - touchEvent || nativeEvent, - context, - props, - state, - ); + if ( + state.pressTarget !== null && + (pointerType !== 'mouse' || + !context.isTargetWithinElement(target, state.pressTarget)) + ) { + // Calculate the responder region we use for deactivation, as the + // element dimensions may have changed since activation. + updateIsPressWithinResponderRegion( + touchEvent || nativeEvent, + context, + props, + state, + ); + } if (state.isPressWithinResponderRegion) { if (isPressed) { @@ -914,6 +895,7 @@ const PressResponder: ReactDOMEventResponder = { if (touchEvent === null) { return; } + state.touchEvent = touchEvent; target = getTouchTarget(context, touchEvent); } else if (type === 'keyup') { // Ignore unrelated keyboard events @@ -961,12 +943,16 @@ const PressResponder: ReactDOMEventResponder = { dispatchPressEndEvents(event, context, props, state); if (state.pressTarget !== null && props.onPress) { - if (!isKeyboardEvent) { + if ( + !isKeyboardEvent && + state.pressTarget !== null && + (pointerType !== 'mouse' || + !context.isTargetWithinElement(target, state.pressTarget)) + ) { // If the event target isn't within the press target, check if we're still // within the responder region. The region may have changed if the // element's layout was modified after activation. updateIsPressWithinResponderRegion( - target, touchEvent || nativeEvent, context, props, @@ -992,6 +978,7 @@ const PressResponder: ReactDOMEventResponder = { } } } + state.touchEvent = null; } else if (type === 'mouseup') { state.ignoreEmulatedMouseEvents = false; } diff --git a/packages/react-events/src/dom/__tests__/Press-test.internal.js b/packages/react-events/src/dom/__tests__/Press-test.internal.js index cc8a36cd37..6ba6dc74e8 100644 --- a/packages/react-events/src/dom/__tests__/Press-test.internal.js +++ b/packages/react-events/src/dom/__tests__/Press-test.internal.js @@ -36,6 +36,12 @@ function createTouchEvent(type, id, data) { identifier: id, }, ], + targetTouches: [ + { + ...data, + identifier: id, + }, + ], }); } @@ -618,14 +624,24 @@ describe('Event responder: Press', () => { ref.current.dispatchEvent( createTouchEvent('touchstart', 0, { target: ref.current, + clientX: 0, + clientY: 0, }), ); ref.current.dispatchEvent( createTouchEvent('touchend', 0, { target: ref.current, + clientX: 0, + clientY: 0, + }), + ); + ref.current.dispatchEvent( + createEvent('pointerup', { + pointerType: 'pen', + clientX: 0, + clientY: 0, }), ); - ref.current.dispatchEvent(createEvent('pointerup', {pointerType: 'pen'})); expect(onPress).toHaveBeenCalledTimes(1); expect(onPress).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'pen', type: 'press'}), @@ -2022,6 +2038,7 @@ describe('Event responder: Press', () => { document.elementFromPoint = () => ref.current; ref.current.dispatchEvent( createTouchEvent('touchend', 0, { + ...coordinatesInside, target: ref.current, }), ); @@ -2269,7 +2286,12 @@ describe('Event responder: Press', () => { ReactDOM.render(element, container); ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent(createEvent('pointerup')); + ref.current.dispatchEvent( + createEvent('pointerup', { + clientX: 0, + clientY: 0, + }), + ); ref.current.dispatchEvent(createEvent('click', {preventDefault})); expect(preventDefault).toBeCalled(); expect(onPress).toHaveBeenCalledWith( @@ -2291,7 +2313,12 @@ describe('Event responder: Press', () => { ReactDOM.render(element, container); buttonRef.current.dispatchEvent(createEvent('pointerdown')); - buttonRef.current.dispatchEvent(createEvent('pointerup')); + buttonRef.current.dispatchEvent( + createEvent('pointerup', { + clientX: 0, + clientY: 0, + }), + ); buttonRef.current.dispatchEvent(createEvent('click', {preventDefault})); expect(preventDefault).toBeCalled(); }); @@ -2310,7 +2337,12 @@ describe('Event responder: Press', () => { ReactDOM.render(element, container); ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent(createEvent('pointerup')); + ref.current.dispatchEvent( + createEvent('pointerup', { + clientX: 0, + clientY: 0, + }), + ); ref.current.dispatchEvent(createEvent('click', {preventDefault})); expect(preventDefault).toBeCalled(); expect(onPress).toHaveBeenCalledWith( @@ -2334,7 +2366,11 @@ describe('Event responder: Press', () => { createEvent('pointerdown', {[modifierKey]: true}), ); ref.current.dispatchEvent( - createEvent('pointerup', {[modifierKey]: true}), + createEvent('pointerup', { + [modifierKey]: true, + clientX: 0, + clientY: 0, + }), ); ref.current.dispatchEvent( createEvent('click', {[modifierKey]: true, preventDefault}), @@ -2358,7 +2394,12 @@ describe('Event responder: Press', () => { ReactDOM.render(element, container); ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent(createEvent('pointerup')); + ref.current.dispatchEvent( + createEvent('pointerup', { + clientX: 0, + clientY: 0, + }), + ); ref.current.dispatchEvent(createEvent('click', {preventDefault})); expect(preventDefault).not.toBeCalled(); expect(onPress).toHaveBeenCalledWith( @@ -2694,7 +2735,10 @@ describe('Event responder: Press', () => { }); function dispatchEventWithTimeStamp(elem, name, timeStamp) { - const event = createEvent(name); + const event = createEvent(name, { + clientX: 0, + clientY: 0, + }); Object.defineProperty(event, 'timeStamp', { value: timeStamp, });