diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSPointerDispatcher.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSPointerDispatcher.java index 8c10504d669..897f5a21785 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSPointerDispatcher.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSPointerDispatcher.java @@ -228,14 +228,6 @@ public class JSPointerDispatcher { TouchTargetHelper.ViewTarget activeViewTarget = hitPath.get(0); int activeTargetTag = activeViewTarget.getViewId(); - if (action == MotionEvent.ACTION_HOVER_MOVE) { - onMove(motionEvent, eventDispatcher, surfaceId, hitPath, targetCoordinates); - return; - } - - // TODO(luwe) - Update this to properly handle native gesture handling for non-hover move events - // If the touch was intercepted by a child, we've already sent a cancel event to JS for this - // gesture, so we shouldn't send any more pointer events related to it. if (mChildHandlingNativeGesture != -1) { return; } @@ -246,6 +238,11 @@ public class JSPointerDispatcher { onDown( activeTargetTag, hitPath, surfaceId, motionEvent, eventDispatcher, targetCoordinates); break; + case MotionEvent.ACTION_HOVER_MOVE: + // TODO(luwe) - converge this with ACTION_MOVE + onMove( + activeTargetTag, motionEvent, eventDispatcher, surfaceId, hitPath, targetCoordinates); + break; case MotionEvent.ACTION_MOVE: // TODO(luwe) - converge this with ACTION_HOVER_MOVE int coalescingKey = mTouchEventCoalescingKeyHelper.getCoalescingKey(mDownStartTime); @@ -343,6 +340,7 @@ public class JSPointerDispatcher { // called on hover_move motion events only private void onMove( + int targetTag, MotionEvent motionEvent, EventDispatcher eventDispatcher, int surfaceId, @@ -383,24 +381,6 @@ public class JSPointerDispatcher { mTouchEventCoalescingKeyHelper.addCoalescingKey(mHoverInteractionKey); } - // If child is handling, eliminate target tags under handling child - if (mChildHandlingNativeGesture > 0) { - int index = 0; - for (ViewTarget viewTarget : hitPath) { - if (viewTarget.getViewId() == mChildHandlingNativeGesture) { - hitPath.subList(0, index).clear(); - break; - } - index++; - } - } - - int targetTag = hitPath.isEmpty() ? -1 : hitPath.get(0).getViewId(); - // If targetTag is empty, we should bail? - if (targetTag == -1) { - return; - } - // hitState is list ordered from inner child -> parent tag // Traverse hitState back-to-front to find the first divergence with lastHitPath // FIXME: this may generate incorrect events when view collapsing changes the hierarchy @@ -552,6 +532,8 @@ public class JSPointerDispatcher { mPrimaryPointerId)); } + // TODO(luwe) - Need to fire pointer out here as well: + // https://w3c.github.io/pointerevents/#dfn-suppress-a-pointer-event-stream List leaveViewTargets = filterByShouldDispatch(hitPath, EVENT.LEAVE, EVENT.LEAVE_CAPTURE, false); diff --git a/packages/rn-tester/js/examples/Experimental/Compatibility/CompatibilityNativeGestureHandling.js b/packages/rn-tester/js/examples/Experimental/Compatibility/CompatibilityNativeGestureHandling.js new file mode 100644 index 00000000000..4494a382640 --- /dev/null +++ b/packages/rn-tester/js/examples/Experimental/Compatibility/CompatibilityNativeGestureHandling.js @@ -0,0 +1,64 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +import * as React from 'react'; +import {ScrollView, View, StyleSheet} from 'react-native'; +import EventfulView from '../W3CPointerEventsEventfulView'; +import type {RNTesterModuleExample} from '../../../types/RNTesterTypes'; + +const styles = StyleSheet.create({ + container: { + borderWidth: 1, + }, + eventfulView: { + borderWidth: 1, + height: 100, + width: '100%', + }, + lighblue: { + backgroundColor: 'lightblue', + }, + item: { + height: 40, + }, +}); + +function CompatibilityNativeGestureHandling(): React.Node { + return ( + + + {Array(100) + .fill() + .map((_, index) => { + return ( + + ); + })} + + + ); +} + +export default ({ + name: 'compatibility_native_gesture', + title: 'Native Gesture Handling Example', + description: + 'Scroll to trigger a native gesture. Verify no native events are being fired once a native gesture starts until it ends. A pointer cancel will be triggered when a native gesture starts.', + render(): React.Node { + return ; + }, +}: RNTesterModuleExample); diff --git a/packages/rn-tester/js/examples/Experimental/W3CPointerEventsEventfulView.js b/packages/rn-tester/js/examples/Experimental/W3CPointerEventsEventfulView.js new file mode 100644 index 00000000000..157f5928587 --- /dev/null +++ b/packages/rn-tester/js/examples/Experimental/W3CPointerEventsEventfulView.js @@ -0,0 +1,118 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +import type {PointerEvent} from 'react-native/Libraries/Types/CoreEventTypes'; +import {StyleSheet, View, Text} from 'react-native'; +import * as React from 'react'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; + +export default function EventfulView(props: {| + name: string, + emitByDefault?: boolean, + onLeave?: boolean, + onLeaveCapture?: boolean, + onEnter?: boolean, + onEnterCapture?: boolean, + onDown?: boolean, + onDownCapture?: boolean, + onUp?: boolean, + onOver?: boolean, + onOverCapture?: boolean, + onOut?: boolean, + onOutCapture?: boolean, + onUpCapture?: boolean, + onMove?: boolean, + onMoveCapture?: boolean, + onCancel?: boolean, + onCancelCapture?: boolean, + log: string => void, + ...ViewProps, +|}): React.Node { + const ref = React.useRef>(); + React.useEffect(() => { + // $FlowFixMe[prop-missing] Using private property + setTag(ref.current?._nativeTag); + }, [ref]); + + const { + log, + name, + children, + emitByDefault, + onLeave, + onLeaveCapture, + onEnter, + onEnterCapture, + onDown, + onDownCapture, + onUp, + onUpCapture, + onMove, + onMoveCapture, + onOut, + onOutCapture, + onOver, + onOverCapture, + onCancel, + onCancelCapture, + ...restProps + } = props; + const [tag, setTag] = React.useState(''); + + const eventLog = + (eventName: string, handler: ?(e: PointerEvent) => void) => + (event: PointerEvent) => { + // $FlowFixMe Using private property + log(`${name} - ${eventName} - target: ${event.target._nativeTag}`); + handler?.(event); + }; + + const listeners = { + onPointerUp: onUp ? eventLog('up') : null, + onPointerUpCapture: onUpCapture ? eventLog('up capture') : null, + onPointerDown: onDown ? eventLog('down') : null, + onPointerDownCapture: onDownCapture ? eventLog('down capture') : null, + onPointerLeave: onLeave ? eventLog('leave') : null, + onPointerLeaveCapture: onLeaveCapture ? eventLog('leave capture') : null, + onPointerEnter: onEnter ? eventLog('enter') : null, + onPointerEnterCapture: onEnterCapture ? eventLog('enter capture') : null, + onPointerMove: onMove ? eventLog('move') : null, + onPointerMoveCapture: onMoveCapture ? eventLog('move capture') : null, + onPointerOut: onOut ? eventLog('out') : null, + onPointerOutCapture: onOutCapture ? eventLog('out capture') : null, + onPointerOver: onOver ? eventLog('over') : null, + onPointerOverCapture: onOverCapture ? eventLog('over capture') : null, + onPointerCancel: onCancel ? eventLog('cancel') : null, + onPointerCancelCapture: onCancelCapture ? eventLog('cancel capture') : null, + }; + + const listeningTo = Object.keys(listeners) + .filter(listenerName => listeners[listenerName] != null) + .join(', '); + + return ( + + + + {props.name}, {tag}, {listeningTo} + + + {props.children} + + ); +} + +const styles = StyleSheet.create({ + row: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + }, +}); diff --git a/packages/rn-tester/js/examples/Experimental/W3CPointerEventsExample.js b/packages/rn-tester/js/examples/Experimental/W3CPointerEventsExample.js index 957fb15e797..67ffe0e25c0 100644 --- a/packages/rn-tester/js/examples/Experimental/W3CPointerEventsExample.js +++ b/packages/rn-tester/js/examples/Experimental/W3CPointerEventsExample.js @@ -8,107 +8,17 @@ * @flow */ -import type {PointerEvent} from 'react-native/Libraries/Types/CoreEventTypes'; import {Button, StyleSheet, ScrollView, View, Text} from 'react-native'; import * as React from 'react'; -import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; import PointerEventAttributesHoverablePointers from './W3CPointerEventPlatformTests/PointerEventAttributesHoverablePointers'; import PointerEventPointerMove from './W3CPointerEventPlatformTests/PointerEventPointerMove'; import CompatibilityAnimatedPointerMove from './Compatibility/CompatibilityAnimatedPointerMove'; +import CompatibilityNativeGestureHandling from './Compatibility/CompatibilityNativeGestureHandling'; import PointerEventPrimaryTouchPointer from './W3CPointerEventPlatformTests/PointerEventPrimaryTouchPointer'; import PointerEventAttributesNoHoverPointers from './W3CPointerEventPlatformTests/PointerEventAttributesNoHoverPointers'; import PointerEventPointerMoveOnChordedMouseButton from './W3CPointerEventPlatformTests/PointerEventPointerMoveOnChordedMouseButton'; - -function EventfulView(props: {| - name: string, - emitByDefault?: boolean, - onLeave?: boolean, - onLeaveCapture?: boolean, - onEnter?: boolean, - onEnterCapture?: boolean, - onDown?: boolean, - onDownCapture?: boolean, - onUp?: boolean, - onOver?: boolean, - onOverCapture?: boolean, - onOut?: boolean, - onOutCapture?: boolean, - onUpCapture?: boolean, - onMove?: boolean, - onMoveCapture?: boolean, - log: string => void, - ...ViewProps, -|}) { - const ref = React.useRef>(); - React.useEffect(() => { - // $FlowFixMe[prop-missing] Using private property - setTag(ref.current?._nativeTag); - }, [ref]); - - const { - log, - name, - children, - emitByDefault, - onLeave, - onLeaveCapture, - onEnter, - onEnterCapture, - onDown, - onDownCapture, - onUp, - onUpCapture, - onMove, - onMoveCapture, - onOut, - onOutCapture, - onOver, - onOverCapture, - ...restProps - } = props; - const [tag, setTag] = React.useState(''); - - const eventLog = - (eventName: string, handler: ?(e: PointerEvent) => void) => - (event: PointerEvent) => { - // $FlowFixMe Using private property - log(`${name} - ${eventName} - target: ${event.target._nativeTag}`); - handler?.(event); - }; - - const listeners = { - onPointerUp: onUp ? eventLog('up') : null, - onPointerUpCapture: onUpCapture ? eventLog('up capture') : null, - onPointerDown: onDown ? eventLog('down') : null, - onPointerDownCapture: onDownCapture ? eventLog('down capture') : null, - onPointerLeave: onLeave ? eventLog('leave') : null, - onPointerLeaveCapture: onLeaveCapture ? eventLog('leave capture') : null, - onPointerEnter: onEnter ? eventLog('enter') : null, - onPointerEnterCapture: onEnterCapture ? eventLog('enter capture') : null, - onPointerMove: onMove ? eventLog('move') : null, - onPointerMoveCapture: onMoveCapture ? eventLog('move capture') : null, - onPointerOut: onOut ? eventLog('out') : null, - onPointerOutCapture: onOutCapture ? eventLog('out capture') : null, - onPointerOver: onOver ? eventLog('over') : null, - onPointerOverCapture: onOverCapture ? eventLog('over capture') : null, - }; - - const listeningTo = Object.keys(listeners) - .filter(listenerName => listeners[listenerName] != null) - .join(', '); - - return ( - - - - {props.name}, {tag}, {listeningTo} - - - {props.children} - - ); -} +import EventfulView from './W3CPointerEventsEventfulView'; function AbsoluteChildExample({log}: {log: string => void}) { return ( @@ -293,5 +203,6 @@ export default { }, }, CompatibilityAnimatedPointerMove, + CompatibilityNativeGestureHandling, ], };