From a9a7382d95a9e44cd97ff47464262df5af2ab358 Mon Sep 17 00:00:00 2001 From: Soe Lynn Date: Wed, 1 May 2024 00:51:59 -0700 Subject: [PATCH] Fix findNodeAtPoint returns incorrect view Summary: This work is based on Ruslan's https://www.internalfb.com/intern/diff/D56185630/ Changelog: [Internal] `Expectation`: In React DevTools, user should be able to select an element on screen and it will show you what React component rendered it. This doesn't work in RN app that is using JS navigation `Root Cause`: In Fabric, when we try to find `ShadowNode` in the `ShadowTree`, `pointerEvents` props are not considered during the lookup of node using coordinate. Hence, in React DevTools when we inspect element, it was hightlighting the overlay `View` with `pointerEvents` props `box-none` was getting highlighted instead of its children view in the hierarchy. Reviewed By: javache Differential Revision: D56334314 fbshipit-source-id: ebfe58c5a1516add347c2c21ab5d075f804df8a9 --- .../components/view/ConcreteViewShadowNode.h | 14 ++ .../renderer/core/LayoutableShadowNode.cpp | 18 ++- .../renderer/core/LayoutableShadowNode.h | 3 + .../core/tests/FindNodeAtPointTest.cpp | 140 ++++++++++++++++++ 4 files changed, 174 insertions(+), 1 deletion(-) diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ConcreteViewShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/view/ConcreteViewShadowNode.h index 299b8db6ff3..370ff6026cc 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ConcreteViewShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ConcreteViewShadowNode.h @@ -84,6 +84,20 @@ class ConcreteViewShadowNode : public ConcreteShadowNode< return BaseShadowNode::getConcreteProps().resolveTransform(layoutMetrics); } + bool canBeTouchTarget() const override { + auto pointerEvents = + BaseShadowNode::getConcreteProps().ViewProps::pointerEvents; + return pointerEvents == PointerEventsMode::Auto || + pointerEvents == PointerEventsMode::BoxOnly; + } + + bool canChildrenBeTouchTarget() const override { + auto pointerEvents = + BaseShadowNode::getConcreteProps().ViewProps::pointerEvents; + return pointerEvents == PointerEventsMode::Auto || + pointerEvents == PointerEventsMode::BoxNone; + } + private: void initialize() noexcept { auto& props = BaseShadowNode::getConcreteProps(); diff --git a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp index 8ea6f8f8223..077ed241717 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp @@ -262,6 +262,14 @@ Point LayoutableShadowNode::getContentOriginOffset() const { return {0, 0}; } +bool LayoutableShadowNode::canBeTouchTarget() const { + return false; +} + +bool LayoutableShadowNode::canChildrenBeTouchTarget() const { + return true; +} + LayoutableShadowNode::UnsharedList LayoutableShadowNode::getLayoutableChildNodes() const { LayoutableShadowNode::UnsharedList layoutableChildren; @@ -314,12 +322,20 @@ ShadowNode::Shared LayoutableShadowNode::findNodeAtPoint( if (layoutableShadowNode == nullptr) { return nullptr; } + + if (!layoutableShadowNode->canBeTouchTarget() && + !layoutableShadowNode->canChildrenBeTouchTarget()) { + return nullptr; + } + auto frame = layoutableShadowNode->getLayoutMetrics().frame; auto transformedFrame = frame * layoutableShadowNode->getTransform(); auto isPointInside = transformedFrame.containsPoint(point); if (!isPointInside) { return nullptr; + } else if (!layoutableShadowNode->canChildrenBeTouchTarget()) { + return node; } auto newPoint = point - transformedFrame.origin - @@ -340,7 +356,7 @@ ShadowNode::Shared LayoutableShadowNode::findNodeAtPoint( return hitView; } } - return isPointInside ? node : nullptr; + return layoutableShadowNode->canBeTouchTarget() ? node : nullptr; } #if RN_DEBUG_STRING_CONVERTIBLE diff --git a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h index f23d931c137..cae02ce15d1 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h @@ -153,6 +153,9 @@ class LayoutableShadowNode : public ShadowNode { virtual Float firstBaseline(Size size) const; virtual Float lastBaseline(Size size) const; + virtual bool canBeTouchTarget() const; + virtual bool canChildrenBeTouchTarget() const; + /* * Returns layoutable children to iterate on. */ diff --git a/packages/react-native/ReactCommon/react/renderer/core/tests/FindNodeAtPointTest.cpp b/packages/react-native/ReactCommon/react/renderer/core/tests/FindNodeAtPointTest.cpp index c5f74fd84fb..0b95fc5443b 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/tests/FindNodeAtPointTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/tests/FindNodeAtPointTest.cpp @@ -226,3 +226,143 @@ TEST(FindNodeAtPointTest, overlappingViewsWithZIndex) { EXPECT_EQ( LayoutableShadowNode::findNodeAtPoint(parentShadowNode, {50, 50})->getTag(), 2); } + +TEST(FindNodeAtPointTest, overlappingViewsWithParentPointerEventsBoxOnly) { + auto builder = simpleComponentBuilder(); + + // clang-format off + auto element = + Element() + .tag(1) + .props([] { + auto sharedProps = std::make_shared(); + sharedProps->pointerEvents = PointerEventsMode::BoxOnly; + return sharedProps; + }) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.size = {100, 100}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .children({ + Element() + .tag(2) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {50, 50}; + layoutMetrics.frame.size = {50, 50}; + shadowNode.setLayoutMetrics(layoutMetrics); + }), + Element() + .tag(3) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {50, 50}; + layoutMetrics.frame.size = {50, 50}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + }); + + auto parentShadowNode = builder.build(element); + + EXPECT_EQ( + LayoutableShadowNode::findNodeAtPoint(parentShadowNode, {60, 60})->getTag(), 1); +} + +TEST(FindNodeAtPointTest, overlappingViewsWithParentPointerEventsBoxNone) { + auto builder = simpleComponentBuilder(); + + // clang-format off + auto element = + Element() + .tag(1) + .props([] { + auto sharedProps = std::make_shared(); + sharedProps->pointerEvents = PointerEventsMode::BoxNone; + return sharedProps; + }) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.size = {100, 100}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .children({ + Element() + .tag(2) + .props([] { + auto sharedProps = std::make_shared(); + sharedProps->zIndex = 1; + auto &yogaStyle = sharedProps->yogaStyle; + yogaStyle.setPositionType(yoga::PositionType::Absolute); + return sharedProps; + }) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {25, 25}; + layoutMetrics.frame.size = {50, 50}; + shadowNode.setLayoutMetrics(layoutMetrics); + }), + Element() + .tag(3) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {50, 50}; + layoutMetrics.frame.size = {50, 50}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + }); + + auto parentShadowNode = builder.build(element); + + EXPECT_EQ( + LayoutableShadowNode::findNodeAtPoint(parentShadowNode, {50, 50})->getTag(), 2); +} + +TEST(FindNodeAtPointTest, overlappingViewsWithParentPointerEventsNone) { + auto builder = simpleComponentBuilder(); + + // clang-format off + auto element = + Element() + .tag(1) + .props([] { + auto sharedProps = std::make_shared(); + sharedProps->pointerEvents = PointerEventsMode::None; + return sharedProps; + }) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.size = {100, 100}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .children({ + Element() + .tag(2) + .props([] { + auto sharedProps = std::make_shared(); + sharedProps->zIndex = 1; + auto &yogaStyle = sharedProps->yogaStyle; + yogaStyle.setPositionType(yoga::PositionType::Absolute); + return sharedProps; + }) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {25, 25}; + layoutMetrics.frame.size = {50, 50}; + shadowNode.setLayoutMetrics(layoutMetrics); + }), + Element() + .tag(3) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {50, 50}; + layoutMetrics.frame.size = {50, 50}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + }); + + auto parentShadowNode = builder.build(element); + + EXPECT_EQ( + LayoutableShadowNode::findNodeAtPoint(parentShadowNode, {50, 50}), nullptr); +}