diff --git a/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm b/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm index 2e7338b3aebcea..dbf5c8cf85e4a1 100644 --- a/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm +++ b/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm @@ -396,28 +396,6 @@ static void UpdateActivePointerWithUITouch( activePointer.modifierFlags = uiEvent.modifierFlags; } -static BOOL IsViewListeningToEvent(RCTReactTaggedView *taggedView, ViewEvents::Offset eventType) -{ - UIView *view = taggedView.view; - if (view && [view.class conformsToProtocol:@protocol(RCTComponentViewProtocol)]) { - auto props = ((id)view).props; - if (SharedViewProps viewProps = std::dynamic_pointer_cast(props)) { - return viewProps->events[eventType]; - } - } - return NO; -} - -static BOOL IsAnyViewInPathListeningToEvent(NSOrderedSet *viewPath, ViewEvents::Offset eventType) -{ - for (RCTReactTaggedView *taggedView in viewPath) { - if (IsViewListeningToEvent(taggedView, eventType)) { - return YES; - } - } - return NO; -} - /** * Given an ActivePointer determine if it is still within the same event target tree as * the one which initiated the pointer gesture. @@ -634,8 +612,7 @@ - (void)_dispatchActivePointers:(std::vector)activePointers event { for (const auto &activePointer : activePointers) { PointerEvent pointerEvent = CreatePointerEventFromActivePointer(activePointer, eventType, _rootComponentView); - NSOrderedSet *eventPathViews = [self handleIncomingPointerEvent:pointerEvent - onView:activePointer.componentView]; + [self handleIncomingPointerEvent:pointerEvent onView:activePointer.componentView]; SharedTouchEventEmitter eventEmitter = GetTouchEmitterFromView( activePointer.componentView, @@ -648,12 +625,7 @@ - (void)_dispatchActivePointers:(std::vector)activePointers event break; } case RCTPointerEventTypeMove: { - BOOL hasMoveEventListeners = - IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerMove) || - IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerMoveCapture); - if (hasMoveEventListeners) { - eventEmitter->onPointerMove(pointerEvent); - } + eventEmitter->onPointerMove(pointerEvent); break; } case RCTPointerEventTypeEnd: { @@ -792,11 +764,9 @@ - (void)hovering:(UIHoverGestureRecognizer *)recognizer PointerEvent event = CreatePointerEventFromIncompleteHoverData( pointerId, pointerType, clientLocation, screenLocation, offsetLocation, modifierFlags); - NSOrderedSet *eventPathViews = [self handleIncomingPointerEvent:event onView:targetView]; + [self handleIncomingPointerEvent:event onView:targetView]; SharedTouchEventEmitter eventEmitter = GetTouchEmitterFromView(targetView, offsetLocation); - BOOL hasMoveEventListeners = IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerMove) || - IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerMoveCapture); - if (eventEmitter != nil && hasMoveEventListeners) { + if (eventEmitter != nil) { eventEmitter->onPointerMove(event); } } @@ -831,10 +801,9 @@ - (void)hovering:(UIHoverGestureRecognizer *)recognizer // Out if (prevTargetView != nil && prevTargetTaggedView.tag != targetTaggedView.tag) { - BOOL shouldEmitOutEvent = IsAnyViewInPathListeningToEvent(currentlyHoveredViews, ViewEvents::Offset::PointerOut); SharedTouchEventEmitter eventEmitter = GetTouchEmitterFromView(prevTargetView, [_rootComponentView convertPoint:clientLocation toView:prevTargetView]); - if (shouldEmitOutEvent && eventEmitter != nil) { + if (eventEmitter != nil) { eventEmitter->onPointerOut(event); } } @@ -847,20 +816,14 @@ - (void)hovering:(UIHoverGestureRecognizer *)recognizer // we reverse iterate (now from target to root), actually emitting the events. NSMutableOrderedSet *viewsToEmitLeaveEventsTo = [NSMutableOrderedSet orderedSet]; - BOOL hasParentLeaveListener = NO; for (RCTReactTaggedView *taggedView in [currentlyHoveredViews reverseObjectEnumerator]) { UIView *componentView = taggedView.view; - BOOL shouldEmitEvent = componentView != nil && - (hasParentLeaveListener || IsViewListeningToEvent(taggedView, ViewEvents::Offset::PointerLeave)); + BOOL shouldEmitEvent = componentView != nil; if (shouldEmitEvent && ![eventPathViews containsObject:taggedView]) { [viewsToEmitLeaveEventsTo addObject:componentView]; } - - if (shouldEmitEvent && !hasParentLeaveListener) { - hasParentLeaveListener = YES; - } } for (UIView *componentView in [viewsToEmitLeaveEventsTo reverseObjectEnumerator]) { @@ -873,10 +836,9 @@ - (void)hovering:(UIHoverGestureRecognizer *)recognizer // Over if (targetView != nil && prevTargetTaggedView.tag != targetTaggedView.tag) { - BOOL shouldEmitOverEvent = IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerOver); SharedTouchEventEmitter eventEmitter = GetTouchEmitterFromView(targetView, [_rootComponentView convertPoint:clientLocation toView:targetView]); - if (shouldEmitOverEvent && eventEmitter != nil) { + if (eventEmitter != nil) { eventEmitter->onPointerOver(event); } } @@ -888,12 +850,10 @@ - (void)hovering:(UIHoverGestureRecognizer *)recognizer // or if one of its parents is listening in case those listeners care about the capturing phase. Adding the ability // for native to distinguish between capturing listeners and not could be an optimization to further reduce the number // of events we send to JS - BOOL hasParentEnterListener = NO; for (RCTReactTaggedView *taggedView in [eventPathViews reverseObjectEnumerator]) { UIView *componentView = taggedView.view; - BOOL shouldEmitEvent = componentView != nil && - (hasParentEnterListener || IsViewListeningToEvent(taggedView, ViewEvents::Offset::PointerEnter)); + BOOL shouldEmitEvent = componentView != nil; if (shouldEmitEvent && ![currentlyHoveredViews containsObject:taggedView]) { SharedTouchEventEmitter eventEmitter = @@ -902,10 +862,6 @@ - (void)hovering:(UIHoverGestureRecognizer *)recognizer eventEmitter->onPointerEnter(event); } } - - if (shouldEmitEvent && !hasParentEnterListener) { - hasParentEnterListener = YES; - } } [_currentlyHoveredViewsPerPointer setObject:eventPathViews forKey:@(pointerId)]; diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h b/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h index e3591f7c41f7e3..0025e28b5b575e 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h @@ -62,6 +62,10 @@ struct ViewEvents { ClickCapture = 31, GotPointerCapture = 32, LostPointerCapture = 33, + PointerDown = 34, + PointerDownCapture = 35, + PointerUp = 36, + PointerUpCapture = 37, }; constexpr bool operator[](const Offset offset) const { diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/propsConversions.h b/packages/react-native/ReactCommon/react/renderer/components/view/propsConversions.h index 46f3801c599057..a59feff2e86efc 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/propsConversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/propsConversions.h @@ -616,18 +616,32 @@ static inline ViewEvents convertRawProp( "onClickCapture", sourceValue[Offset::ClickCapture], defaultValue[Offset::ClickCapture]); - result[Offset::GotPointerCapture] = convertRawProp( + result[Offset::PointerDown] = convertRawProp( context, rawProps, - "onGotPointerCapture", - sourceValue[Offset::GotPointerCapture], - defaultValue[Offset::GotPointerCapture]); - result[Offset::LostPointerCapture] = convertRawProp( + "onPointerDown", + sourceValue[Offset::PointerDown], + defaultValue[Offset::PointerDown]); + result[Offset::PointerDownCapture] = convertRawProp( context, rawProps, - "onLostPointerCapture", - sourceValue[Offset::LostPointerCapture], - defaultValue[Offset::LostPointerCapture]); + "onPointerDownCapture", + sourceValue[Offset::PointerDownCapture], + defaultValue[Offset::PointerDownCapture]); + result[Offset::PointerUp] = convertRawProp( + context, + rawProps, + "onPointerUp", + sourceValue[Offset::PointerUp], + defaultValue[Offset::PointerUp]); + result[Offset::PointerUpCapture] = convertRawProp( + context, + rawProps, + "onPointerUpCapture", + sourceValue[Offset::PointerUpCapture], + defaultValue[Offset::PointerUpCapture]); + // TODO: gotPointerCapture & lostPointerCapture (causes issues with + // RawPropsKey for some reason) // PanResponder callbacks result[Offset::MoveShouldSetResponder] = convertRawProp( diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/PointerEventsProcessor.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/PointerEventsProcessor.cpp index 0538f040aca7d0..cbd08e736fa4de 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/PointerEventsProcessor.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/PointerEventsProcessor.cpp @@ -7,9 +7,80 @@ #include "PointerEventsProcessor.h" +#include + namespace facebook::react { -static PointerEventTarget retargetPointerEvent( +static ShadowNode::Shared GetShadowNodeFromEventTarget( + jsi::Runtime &runtime, + EventTarget const &target) { + auto instanceHandle = target.getInstanceHandle(runtime); + if (instanceHandle.isObject()) { + auto handleObj = instanceHandle.asObject(runtime); + if (handleObj.hasProperty(runtime, "stateNode")) { + auto stateNode = handleObj.getProperty(runtime, "stateNode"); + if (stateNode.isObject()) { + auto stateNodeObj = stateNode.asObject(runtime); + if (stateNodeObj.hasProperty(runtime, "node")) { + auto node = stateNodeObj.getProperty(runtime, "node"); + return shadowNodeFromValue(runtime, node); + } + } + } + } + return nullptr; +} + +static bool IsViewListeningToEvents( + ShadowNode const &shadowNode, + std::initializer_list eventTypes) { + if (auto viewShadowNode = traitCast(&shadowNode)) { + auto &viewProps = viewShadowNode->getConcreteProps(); + for (const ViewEvents::Offset eventType : eventTypes) { + if (viewProps.events[eventType]) { + return true; + } + } + } + return false; +} + +static bool IsAnyViewInPathToRootListeningToEvents( + UIManager const &uiManager, + ShadowNode const &shadowNode, + std::initializer_list eventTypes) { + // Check the target view first + if (IsViewListeningToEvents(shadowNode, eventTypes)) { + return true; + } + + // Retrieve the node's root & a list of nodes between the target and the root + auto owningRootShadowNode = ShadowNode::Shared{}; + uiManager.getShadowTreeRegistry().visit( + shadowNode.getSurfaceId(), + [&owningRootShadowNode](ShadowTree const &shadowTree) { + owningRootShadowNode = shadowTree.getCurrentRevision().rootShadowNode; + }); + + if (owningRootShadowNode == nullptr) { + return false; + } + + auto &nodeFamily = shadowNode.getFamily(); + auto ancestors = nodeFamily.getAncestors(*owningRootShadowNode); + + // Check for listeners from the target's parent to the root + for (auto it = ancestors.rbegin(); it != ancestors.rend(); it++) { + auto ¤tNode = it->first.get(); + if (IsViewListeningToEvents(currentNode, eventTypes)) { + return true; + } + } + + return false; +} + +static PointerEventTarget RetargetPointerEvent( PointerEvent const &event, ShadowNode const &nodeToTarget, UIManager const &uiManager) { @@ -59,6 +130,72 @@ static ShadowNode::Shared getCaptureTargetOverride( return maybeTarget.lock(); } +/* + * Centralized method which determines if an event should be sent to JS by + * inspecing the listeners in the target's view path. + */ +static bool ShouldEmitPointerEvent( + ShadowNode const &targetNode, + std::string const &type, + UIManager const &uiManager) { + if (type == "topPointerDown") { + return IsAnyViewInPathToRootListeningToEvents( + uiManager, + targetNode, + {ViewEvents::Offset::PointerDown, + ViewEvents::Offset::PointerDownCapture}); + } else if (type == "topPointerUp") { + return IsAnyViewInPathToRootListeningToEvents( + uiManager, + targetNode, + {ViewEvents::Offset::PointerUp, ViewEvents::Offset::PointerUpCapture}); + } else if (type == "topPointerMove") { + return IsAnyViewInPathToRootListeningToEvents( + uiManager, + targetNode, + {ViewEvents::Offset::PointerMove, + ViewEvents::Offset::PointerMoveCapture}); + } else if (type == "topPointerEnter") { + // This event goes through the capturing phase in full but only bubble + // through the target and no futher up the tree + return IsViewListeningToEvents( + targetNode, {ViewEvents::Offset::PointerEnter}) || + IsAnyViewInPathToRootListeningToEvents( + uiManager, + targetNode, + {ViewEvents::Offset::PointerEnterCapture}); + } else if (type == "topPointerLeave") { + // This event goes through the capturing phase in full but only bubble + // through the target and no futher up the tree + return IsViewListeningToEvents( + targetNode, {ViewEvents::Offset::PointerLeave}) || + IsAnyViewInPathToRootListeningToEvents( + uiManager, + targetNode, + {ViewEvents::Offset::PointerLeaveCapture}); + } else if (type == "topPointerOver") { + return IsAnyViewInPathToRootListeningToEvents( + uiManager, + targetNode, + {ViewEvents::Offset::PointerOver, + ViewEvents::Offset::PointerOverCapture}); + } else if (type == "topPointerOut") { + return IsAnyViewInPathToRootListeningToEvents( + uiManager, + targetNode, + {ViewEvents::Offset::PointerOut, + ViewEvents::Offset::PointerOutCapture}); + } else if (type == "topClick") { + return IsAnyViewInPathToRootListeningToEvents( + uiManager, + targetNode, + {ViewEvents::Offset::Click, ViewEvents::Offset::ClickCapture}); + } + // This is more of an optimization method so if we encounter a type which + // has not been specifically addressed above we should just let it through. + return true; +} + void PointerEventsProcessor::interceptPointerEvent( jsi::Runtime &runtime, EventTarget const *target, @@ -86,7 +223,11 @@ void PointerEventsProcessor::interceptPointerEvent( } eventTarget->retain(runtime); - eventDispatcher(runtime, eventTarget, type, priority, pointerEvent); + auto shadowNode = GetShadowNodeFromEventTarget(runtime, *eventTarget); + if (shadowNode != nullptr && + ShouldEmitPointerEvent(*shadowNode, type, uiManager)) { + eventDispatcher(runtime, eventTarget, type, priority, pointerEvent); + } eventTarget->release(runtime); // Implicit pointer capture release @@ -156,28 +297,38 @@ void PointerEventsProcessor::processPendingPointerCapture( auto activeOverrideTag = (hasActiveOverride) ? activeOverride->getTag() : -1; if (hasActiveOverride && activeOverrideTag != pendingOverrideTag) { - auto retargeted = retargetPointerEvent(event, *activeOverride, uiManager); + auto retargeted = RetargetPointerEvent(event, *activeOverride, uiManager); retargeted.target->retain(runtime); - eventDispatcher( - runtime, - retargeted.target.get(), - "topLostPointerCapture", - ReactEventPriority::Discrete, - retargeted.event); + auto shadowNode = GetShadowNodeFromEventTarget(runtime, *retargeted.target); + if (shadowNode != nullptr && + ShouldEmitPointerEvent( + *shadowNode, "topLostPointerCapture", uiManager)) { + eventDispatcher( + runtime, + retargeted.target.get(), + "topLostPointerCapture", + ReactEventPriority::Discrete, + retargeted.event); + } retargeted.target->release(runtime); } if (hasPendingOverride && activeOverrideTag != pendingOverrideTag) { - auto retargeted = retargetPointerEvent(event, *pendingOverride, uiManager); + auto retargeted = RetargetPointerEvent(event, *pendingOverride, uiManager); retargeted.target->retain(runtime); - eventDispatcher( - runtime, - retargeted.target.get(), - "topGotPointerCapture", - ReactEventPriority::Discrete, - retargeted.event); + auto shadowNode = GetShadowNodeFromEventTarget(runtime, *retargeted.target); + if (shadowNode != nullptr && + ShouldEmitPointerEvent( + *shadowNode, "topGotPointerCapture", uiManager)) { + eventDispatcher( + runtime, + retargeted.target.get(), + "topGotPointerCapture", + ReactEventPriority::Discrete, + retargeted.event); + } retargeted.target->release(runtime); }