From 76d41864a2cb1429ff659f16a4146a478e5c22a2 Mon Sep 17 00:00:00 2001 From: Alex Danoff Date: Thu, 25 May 2023 16:38:53 -0700 Subject: [PATCH] W3CPointerEvents: emit click events based on pointerDown/pointerUp (#37541) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/37541 This diff makes changes to emit bubbling click events (i.e. [these](https://www.w3.org/TR/uievents/#click)) based on the existing pointer-down/pointer-up events. As per the [pointer events spec](https://www.w3.org/TR/pointerevents3/#the-click-auxclick-and-contextmenu-events), click events are still dispatched as PointerEvents (i.e. conform to the PointerEvent interface). Changelog: [Internal] [Added] - W3CPointerEvents: emit click events based on pointerDown/pointerUp Reviewed By: NickGerleman Differential Revision: D45666344 fbshipit-source-id: 08829d3a24fef6851c1c3c29d4fa51b31ec68931 --- .../react/uimanager/BaseViewManager.java | 10 +++++ .../react/uimanager/JSPointerDispatcher.java | 44 +++++++++++++++++++ .../react/uimanager/events/PointerEvent.java | 20 ++++++--- .../uimanager/events/PointerEventHelper.java | 5 +++ 4 files changed, 74 insertions(+), 5 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index dc0828689db..a7970346275 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -700,6 +700,16 @@ public abstract class BaseViewManager> mLastHitPathByPointerId; private Map mLastEventCoordinatesByPointerId; + private Map> mCurrentlyDownPointerIdsToHitPath; private Set mHoveringPointerIds = new HashSet<>(); private int mChildHandlingNativeGesture = -1; @@ -53,6 +54,7 @@ public class JSPointerDispatcher { public JSPointerDispatcher(ViewGroup viewGroup) { mRootViewGroup = viewGroup; + mCurrentlyDownPointerIdsToHitPath = new HashMap<>(); } public void onChildStartedNativeGesture( @@ -87,6 +89,30 @@ public class JSPointerDispatcher { mChildHandlingNativeGesture = -1; } + // returns the section of the hit path shared by both lists, or an empty list if there's no such + // section + private static List findHitPathIntersection( + final List hitsA, final List hitsB) { + if (hitsA.isEmpty()) { + return new ArrayList<>(); + } + if (hitsB.isEmpty()) { + return new ArrayList<>(); + } + + Set inA = new HashSet<>(hitsA); + + List intersection = new ArrayList<>(); + + for (final ViewTarget vt : hitsB) { + if (inA.contains(vt)) { + intersection.add(vt); + } + } + + return intersection; + } + private void onUp( int activeTargetTag, PointerEventState eventState, @@ -127,6 +153,18 @@ public class JSPointerDispatcher { eventDispatcher); } + List hitPathDown = mCurrentlyDownPointerIdsToHitPath.remove(activePointerId); + if (hitPathDown != null + && isAnyoneListeningForBubblingEvent(activeHitPath, EVENT.CLICK, EVENT.CLICK_CAPTURE)) { + List hitPathForClick = findHitPathIntersection(hitPathDown, activeHitPath); + if (!hitPathForClick.isEmpty()) { + final ViewTarget clickTarget = hitPathForClick.get(0); + eventDispatcher.dispatchEvent( + PointerEvent.obtain( + PointerEventHelper.CLICK, clickTarget.getViewId(), eventState, motionEvent)); + } + } + if (motionEvent.getActionMasked() == MotionEvent.ACTION_UP) { mPrimaryPointerId = UNSET_POINTER_ID; } @@ -175,6 +213,12 @@ public class JSPointerDispatcher { eventDispatcher); } + // store some information if we might need to emit a click later on + if (isAnyoneListeningForBubblingEvent(activeHitPath, EVENT.CLICK, EVENT.CLICK_CAPTURE)) { + mCurrentlyDownPointerIdsToHitPath.put( + eventState.getActivePointerId(), new ArrayList<>(activeHitPath)); + } + boolean listeningForDown = isAnyoneListeningForBubblingEvent(activeHitPath, EVENT.DOWN, EVENT.DOWN_CAPTURE); if (listeningForDown) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEvent.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEvent.java index 0e298829a55..02b99115070 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEvent.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEvent.java @@ -91,6 +91,10 @@ public class PointerEvent extends Event { return mEventName; } + private boolean isClickEvent() { + return mEventName.equals(PointerEventHelper.CLICK); + } + @Override public void dispatch(RCTEventEmitter rctEventEmitter) { if (mMotionEvent == null) { @@ -190,7 +194,8 @@ public class PointerEvent extends Event { pointerEvent.putString("pointerType", pointerType); boolean isPrimary = - mEventState.supportsHover(pointerId) || pointerId == mEventState.mPrimaryPointerId; + !isClickEvent() // compatibility click events should not be considered primary + && (mEventState.supportsHover(pointerId) || pointerId == mEventState.mPrimaryPointerId); pointerEvent.putBoolean("isPrimary", isPrimary); // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent @@ -223,8 +228,8 @@ public class PointerEvent extends Event { pointerEvent.putDouble("tiltY", 0); pointerEvent.putInt("twist", 0); - - if (pointerType.equals(PointerEventHelper.POINTER_TYPE_MOUSE)) { + // note: click events should have width = height = 1 + if (pointerType.equals(PointerEventHelper.POINTER_TYPE_MOUSE) || isClickEvent()) { pointerEvent.putDouble("width", 1); pointerEvent.putDouble("height", 1); } else { @@ -241,8 +246,12 @@ public class PointerEvent extends Event { pointerEvent.putInt( "buttons", PointerEventHelper.getButtons(mEventName, pointerType, buttonState)); - pointerEvent.putDouble( - "pressure", PointerEventHelper.getPressure(pointerEvent.getInt("buttons"), mEventName)); + final double pressure = + isClickEvent() // click events need pressure=0 + ? 0 + : PointerEventHelper.getPressure(pointerEvent.getInt("buttons"), mEventName); + + pointerEvent.putDouble("pressure", pressure); pointerEvent.putDouble("tangentialPressure", 0.0); return pointerEvent; @@ -264,6 +273,7 @@ public class PointerEvent extends Event { case PointerEventHelper.POINTER_LEAVE: case PointerEventHelper.POINTER_OUT: case PointerEventHelper.POINTER_OVER: + case PointerEventHelper.CLICK: pointersEventData = Arrays.asList(createW3CPointerEvent(activePointerIndex)); break; } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEventHelper.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEventHelper.java index 757d5f5dd4e..787c9ed117c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEventHelper.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEventHelper.java @@ -25,6 +25,8 @@ public class PointerEventHelper { public static enum EVENT { CANCEL, CANCEL_CAPTURE, + CLICK, + CLICK_CAPTURE, DOWN, DOWN_CAPTURE, ENTER, @@ -49,6 +51,7 @@ public class PointerEventHelper { public static final String POINTER_UP = "topPointerUp"; public static final String POINTER_OVER = "topPointerOver"; public static final String POINTER_OUT = "topPointerOut"; + public static final String CLICK = "topClick"; // https://w3c.github.io/pointerevents/#the-buttons-property public static int getButtons(String eventName, String pointerType, int buttonState) { @@ -117,6 +120,8 @@ public class PointerEventHelper { case UP_CAPTURE: case CANCEL: case CANCEL_CAPTURE: + case CLICK: + case CLICK_CAPTURE: return true; }