diff --git a/Libraries/Components/View/ViewPropTypes.js b/Libraries/Components/View/ViewPropTypes.js index c7ee1baa63a..24c0afcaee8 100644 --- a/Libraries/Components/View/ViewPropTypes.js +++ b/Libraries/Components/View/ViewPropTypes.js @@ -98,6 +98,10 @@ type PointerEventProps = $ReadOnly<{| onPointerDownCapture?: ?(e: PointerEvent) => void, onPointerUp?: ?(e: PointerEvent) => void, onPointerUpCapture?: ?(e: PointerEvent) => void, + onPointerOver?: ?(e: PointerEvent) => void, + onPointerOverCapture?: ?(e: PointerEvent) => void, + onPointerOut?: ?(e: PointerEvent) => void, + onPointerOutCapture?: ?(e: PointerEvent) => void, |}>; type FocusEventProps = $ReadOnly<{| diff --git a/Libraries/NativeComponent/BaseViewConfig.android.js b/Libraries/NativeComponent/BaseViewConfig.android.js index b2b004e06a0..6342ccf4ceb 100644 --- a/Libraries/NativeComponent/BaseViewConfig.android.js +++ b/Libraries/NativeComponent/BaseViewConfig.android.js @@ -90,6 +90,18 @@ const bubblingEventTypes = { bubbled: 'onPointerUp', }, }, + topPointerOut: { + phasedRegistrationNames: { + captured: 'onPointerOutCapture', + bubbled: 'onPointerOut', + }, + }, + topPointerOver: { + phasedRegistrationNames: { + captured: 'onPointerOverCapture', + bubbled: 'onPointerOver', + }, + }, }; const directEventTypes = { diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/viewPropConversions.h b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/viewPropConversions.h index 91e90813d1b..1271fe3e903 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/viewPropConversions.h +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/viewPropConversions.h @@ -58,6 +58,10 @@ constexpr MapBuffer::Key VP_ZINDEX = 34; constexpr MapBuffer::Key VP_POINTER_ENTER_CAPTURE = 38; constexpr MapBuffer::Key VP_POINTER_LEAVE_CAPTURE = 39; constexpr MapBuffer::Key VP_POINTER_MOVE_CAPTURE = 40; +constexpr MapBuffer::Key VP_POINTER_OVER = 41; +constexpr MapBuffer::Key VP_POINTER_OVER_CAPTURE = 42; +constexpr MapBuffer::Key VP_POINTER_OUT = 43; +constexpr MapBuffer::Key VP_POINTER_OUT_CAPTURE = 44; // Yoga values constexpr MapBuffer::Key YG_BORDER_WIDTH = 100; @@ -475,6 +479,17 @@ static inline MapBuffer viewPropsDiff( builder.putBool( VP_POINTER_MOVE_CAPTURE, newProps.events[ViewEvents::Offset::PointerMoveCapture]); + builder.putBool( + VP_POINTER_OVER, newProps.events[ViewEvents::Offset::PointerOver]); + builder.putBool( + VP_POINTER_OVER_CAPTURE, + newProps.events[ViewEvents::Offset::PointerOverCapture]); + + builder.putBool( + VP_POINTER_OUT, newProps.events[ViewEvents::Offset::PointerOut]); + builder.putBool( + VP_POINTER_OUT_CAPTURE, + newProps.events[ViewEvents::Offset::PointerOutCapture]); } if (oldProps.removeClippedSubviews != newProps.removeClippedSubviews) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index e0458a86a7d..8f1239d7456 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -584,6 +584,16 @@ public abstract class BaseViewManager enterViewTargets = filterByShouldDispatch(hitPath, EVENT.ENTER, EVENT.ENTER_CAPTURE, false); @@ -158,6 +172,8 @@ public class JSPointerDispatcher { if (action == MotionEvent.ACTION_POINTER_DOWN) { mTouchEventCoalescingKeyHelper.incrementCoalescingKey(mDownStartTime); + // TODO(luwe) We need to fire indirect over,enter events for secondary pointers + boolean listeningForDown = isAnyoneListeningForBubblingEvent(hitPath, EVENT.DOWN, EVENT.DOWN_CAPTURE); if (listeningForDown) { @@ -198,6 +214,8 @@ public class JSPointerDispatcher { if (action == MotionEvent.ACTION_POINTER_UP) { mTouchEventCoalescingKeyHelper.incrementCoalescingKey(mDownStartTime); + // TODO(luwe) We need to fire indirect out,leave events for secondary pointers + boolean listeningForUp = isAnyoneListeningForBubblingEvent(hitPath, EVENT.UP, EVENT.UP_CAPTURE); if (listeningForUp) { @@ -235,6 +253,19 @@ public class JSPointerDispatcher { } if (!supportsHover) { + boolean listeningForOut = + isAnyoneListeningForBubblingEvent(hitPath, EVENT.OUT, EVENT.OUT_CAPTURE); + if (listeningForOut) { + eventDispatcher.dispatchEvent( + PointerEvent.obtain( + PointerEventHelper.POINTER_OUT, + surfaceId, + activeTargetTag, + motionEvent, + mTargetCoordinates, + mPrimaryPointerId)); + } + List leaveViewTargets = filterByShouldDispatch(hitPath, EVENT.LEAVE, EVENT.LEAVE_CAPTURE, false); @@ -408,6 +439,53 @@ public class JSPointerDispatcher { // If something has changed in either enter/exit, let's start a new coalescing key mTouchEventCoalescingKeyHelper.incrementCoalescingKey(mHoverInteractionKey); + // Out, Leave events + if (mLastHitPath.size() > 0) { + int lastTargetTag = mLastHitPath.get(0).getViewId(); + boolean listeningForOut = + isAnyoneListeningForBubblingEvent(mLastHitPath, EVENT.OUT, EVENT.OUT_CAPTURE); + if (listeningForOut) { + eventDispatcher.dispatchEvent( + PointerEvent.obtain( + PointerEventHelper.POINTER_OUT, + surfaceId, + lastTargetTag, + motionEvent, + mTargetCoordinates, + mPrimaryPointerId)); + } + + // target -> root + List leaveViewTargets = + filterByShouldDispatch( + mLastHitPath.subList(0, mLastHitPath.size() - firstDivergentIndexFromBack), + EVENT.LEAVE, + EVENT.LEAVE_CAPTURE, + nonDivergentListeningToLeave); + if (leaveViewTargets.size() > 0) { + // We want to dispatch from target -> root, so no need to reverse + dispatchEventForViewTargets( + PointerEventHelper.POINTER_LEAVE, + leaveViewTargets, + eventDispatcher, + surfaceId, + motionEvent); + } + } + + boolean listeningForOver = + isAnyoneListeningForBubblingEvent(hitPath, EVENT.OVER, EVENT.OVER_CAPTURE); + if (listeningForOver) { + eventDispatcher.dispatchEvent( + PointerEvent.obtain( + PointerEventHelper.POINTER_OVER, + surfaceId, + targetTag, + motionEvent, + mTargetCoordinates, + mPrimaryPointerId)); + } + // target -> root List enterViewTargets = filterByShouldDispatch( @@ -426,23 +504,6 @@ public class JSPointerDispatcher { surfaceId, motionEvent); } - - // target -> root - List leaveViewTargets = - filterByShouldDispatch( - mLastHitPath.subList(0, mLastHitPath.size() - firstDivergentIndexFromBack), - EVENT.LEAVE, - EVENT.LEAVE_CAPTURE, - nonDivergentListeningToLeave); - if (leaveViewTargets.size() > 0) { - // We want to dispatch from target -> root, so no need to reverse - dispatchEventForViewTargets( - PointerEventHelper.POINTER_LEAVE, - leaveViewTargets, - eventDispatcher, - surfaceId, - motionEvent); - } } int coalescingKey = mTouchEventCoalescingKeyHelper.getCoalescingKey(mHoverInteractionKey); diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEvent.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEvent.java index 21611af0ae9..7ed9c7723b0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEvent.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEvent.java @@ -218,6 +218,8 @@ public class PointerEvent extends Event { case PointerEventHelper.POINTER_DOWN: case PointerEventHelper.POINTER_UP: case PointerEventHelper.POINTER_LEAVE: + case PointerEventHelper.POINTER_OUT: + case PointerEventHelper.POINTER_OVER: pointersEventData = Arrays.asList(createPointerEventData(activePointerIndex)); break; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEventHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEventHelper.java index 1b576137e65..ea7c69409cd 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEventHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEventHelper.java @@ -36,6 +36,10 @@ public class PointerEventHelper { MOVE_CAPTURE, UP, UP_CAPTURE, + OUT, + OUT_CAPTURE, + OVER, + OVER_CAPTURE, }; public static final String POINTER_CANCEL = "topPointerCancel"; @@ -44,6 +48,8 @@ public class PointerEventHelper { public static final String POINTER_LEAVE = "topPointerLeave"; public static final String POINTER_MOVE = "topPointerMove"; public static final String POINTER_UP = "topPointerUp"; + public static final String POINTER_OVER = "topPointerOver"; + public static final String POINTER_OUT = "topPointerOut"; /** We don't dispatch capture events from native; that's currently handled by JS. */ public static @Nullable String getDispatchableEventName(EVENT event) { @@ -60,6 +66,10 @@ public class PointerEventHelper { return PointerEventHelper.POINTER_CANCEL; case UP: return PointerEventHelper.POINTER_UP; + case OVER: + return PointerEventHelper.POINTER_OVER; + case OUT: + return PointerEventHelper.POINTER_OUT; default: FLog.e(ReactConstants.TAG, "No dispatchable event name for type: " + event); return null; @@ -121,6 +131,18 @@ public class PointerEventHelper { case MOVE_CAPTURE: value = view.getTag(R.id.pointer_move_capture); break; + case OVER: + value = view.getTag(R.id.pointer_over); + break; + case OVER_CAPTURE: + value = view.getTag(R.id.pointer_over_capture); + break; + case OUT: + value = view.getTag(R.id.pointer_out); + break; + case OUT_CAPTURE: + value = view.getTag(R.id.pointer_out_capture); + break; } if (value == null) { @@ -147,6 +169,8 @@ public class PointerEventHelper { case POINTER_MOVE: case POINTER_ENTER: case POINTER_LEAVE: + case POINTER_OVER: + case POINTER_OUT: return EventCategoryDef.CONTINUOUS; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactMapBufferPropSetter.kt b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactMapBufferPropSetter.kt index 68b917d0b1b..97c9fb513b0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactMapBufferPropSetter.kt +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactMapBufferPropSetter.kt @@ -58,6 +58,10 @@ object ReactMapBufferPropSetter { private const val VP_POINTER_ENTER_CAPTURE = 38 private const val VP_POINTER_LEAVE_CAPTURE = 39 private const val VP_POINTER_MOVE_CAPTURE = 40 + private const val VP_POINTER_OUT = 41 + private const val VP_POINTER_OUT_CAPTURE = 42 + private const val VP_POINTER_OVER = 43 + private const val VP_POINTER_OVER_CAPTURE = 44 // Yoga values private const val YG_BORDER_WIDTH = 100 @@ -197,6 +201,18 @@ object ReactMapBufferPropSetter { VP_POINTER_MOVE_CAPTURE -> { viewManager.setPointerMoveCapture(view, entry.booleanValue) } + VP_POINTER_OUT -> { + viewManager.setPointerOut(view, entry.booleanValue) + } + VP_POINTER_OUT_CAPTURE -> { + viewManager.setPointerOutCapture(view, entry.booleanValue) + } + VP_POINTER_OVER -> { + viewManager.setPointerOver(view, entry.booleanValue) + } + VP_POINTER_OVER_CAPTURE -> { + viewManager.setPointerOverCapture(view, entry.booleanValue) + } VP_REMOVE_CLIPPED_SUBVIEW -> { viewManager.setRemoveClippedSubviews(view, entry.booleanValue) } diff --git a/ReactAndroid/src/main/res/views/uimanager/values/ids.xml b/ReactAndroid/src/main/res/views/uimanager/values/ids.xml index 405ca4d7af9..f62f6cfa74f 100644 --- a/ReactAndroid/src/main/res/views/uimanager/values/ids.xml +++ b/ReactAndroid/src/main/res/views/uimanager/values/ids.xml @@ -50,4 +50,12 @@ + + + + + + + + diff --git a/ReactCommon/react/renderer/components/view/primitives.h b/ReactCommon/react/renderer/components/view/primitives.h index 10fe3bf19c5..1ecf4415626 100644 --- a/ReactCommon/react/renderer/components/view/primitives.h +++ b/ReactCommon/react/renderer/components/view/primitives.h @@ -56,6 +56,9 @@ struct ViewEvents { PointerMoveCapture = 25, PointerOver = 26, PointerOut = 27, + PointerOverCapture = 28, + PointerOutCapture = 29, + }; constexpr bool operator[](const Offset offset) const { diff --git a/packages/rn-tester/js/examples/Experimental/W3CPointerEventsExample.js b/packages/rn-tester/js/examples/Experimental/W3CPointerEventsExample.js index 3fbc4e4c0c8..2a6b6458931 100644 --- a/packages/rn-tester/js/examples/Experimental/W3CPointerEventsExample.js +++ b/packages/rn-tester/js/examples/Experimental/W3CPointerEventsExample.js @@ -29,6 +29,10 @@ function EventfulView(props: {| onDown?: boolean, onDownCapture?: boolean, onUp?: boolean, + onOver?: boolean, + onOverCapture?: boolean, + onOut?: boolean, + onOutCapture?: boolean, onUpCapture?: boolean, onMove?: boolean, onMoveCapture?: boolean, @@ -56,6 +60,10 @@ function EventfulView(props: {| onUpCapture, onMove, onMoveCapture, + onOut, + onOutCapture, + onOver, + onOverCapture, ...restProps } = props; const [tag, setTag] = React.useState(''); @@ -79,6 +87,10 @@ function EventfulView(props: {| 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) @@ -130,6 +142,8 @@ function RelativeChildExample({log}: {log: string => void}) { log={log} style={StyleSheet.compose(styles.eventfulView, styles.parent)} onUp + onOver + onOut onDown onEnter onLeave @@ -137,6 +151,8 @@ function RelativeChildExample({log}: {log: string => void}) { void}) {