From d8bc7c68c0a6ea2dee73edc3270dbdcb3b132af9 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Thu, 27 Feb 2025 03:20:01 -0800 Subject: [PATCH] fix[LayoutableShadowNode::findNodeAtPoint]: consider layoutMetrics.overflowInset (#49701) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49701 # Changelog: [General] [Fixed] - findNodeAtPoint now considers overflow area of the parent node This may happen when the child overflows the parent without resizing it. For example, when child has `position: 'absolute'` and absolute coordinates. See - https://fb.workplace.com/groups/rn.debugger.feedback/posts/1075015864052657/ - https://fb.workplace.com/groups/rn.debugger.feedback/permalink/1180445730176336/ - https://fb.workplace.com/groups/ford.eng.discussions/permalink/1961390260971262/ for more context Reviewed By: NickGerleman Differential Revision: D63328182 fbshipit-source-id: 9d5ed3ec8d88d8d65455aa3664588dc7d7273888 --- .../renderer/core/LayoutableShadowNode.cpp | 17 +++++++--- .../core/tests/FindNodeAtPointTest.cpp | 34 +++++++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp index 86e78064cb6..565c1a2e0f5 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp @@ -261,15 +261,22 @@ ShadowNode::Shared LayoutableShadowNode::findNodeAtPoint( return nullptr; } + auto layoutMetrics = layoutableShadowNode->getLayoutMetrics(); auto transform = layoutableShadowNode->getTransform(); - auto frame = layoutableShadowNode->getLayoutMetrics().frame; - auto transformedFrame = frame * transform; + auto transformedFrame = layoutMetrics.frame * transform; auto isPointInside = transformedFrame.containsPoint(point); - if (!isPointInside) { - return nullptr; - } else if (!layoutableShadowNode->canChildrenBeTouchTarget()) { + if (isPointInside && !layoutableShadowNode->canChildrenBeTouchTarget()) { return node; + } else if (!isPointInside) { + auto overflowFrame = + insetBy(layoutMetrics.frame, layoutMetrics.overflowInset); + auto transformedOverflowFrame = overflowFrame * transform; + // If child overflows parent, the touch may be intercepted by the child + // only, so we should continue recursing. + if (!transformedOverflowFrame.containsPoint(point)) { + return nullptr; + } } if (Transform::isVerticalInversion(transform) || 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 c167f86591a..0776acf8983 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/tests/FindNodeAtPointTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/tests/FindNodeAtPointTest.cpp @@ -415,3 +415,37 @@ TEST(FindNodeAtPointTest, invertedList) { ->getTag(), 2); } + +TEST(FindNodeAtPointTest, considersOverflowAreaOfTheParent) { + auto builder = simpleComponentBuilder(); + + auto element = + Element() + .tag(1) + .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.size = {100, 0}; + layoutMetrics.overflowInset = {0, 0, 0, -100}; + + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .children({Element().tag(3).finalize( + [](ViewShadowNode& shadowNode) { + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.size = {100, 100}; + shadowNode.setLayoutMetrics(layoutMetrics); + })})}); + + auto parentShadowNode = builder.build(element); + EXPECT_EQ( + LayoutableShadowNode::findNodeAtPoint(parentShadowNode, {1, 99}) + ->getTag(), + 3); +}