diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt index b1b3f12f50c2..29bdbab9e781 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt @@ -57,7 +57,7 @@ internal class FabricUIManagerBinding : HybridClassBase() { external fun findNextFocusableElement(parentTag: Int, focusedTag: Int, direction: Int): Int - external fun getRelativeAncestorList(rootTag: Int, childTag: Int): IntArray + external fun getRelativeAncestorList(rootTag: Int, childTag: Int): IntArray? external fun stopSurface(surfaceId: Int) diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp index d5c154f1e6f1..c28f1291f5ef 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp @@ -343,25 +343,18 @@ jintArray FabricUIManagerBinding::getRelativeAncestorList( return nullptr; } - ShadowNode::AncestorList ancestorList = - childShadowNode->getFamily().getAncestors(*rootShadowNode); + std::optional> ancestorTags = + FocusOrderingHelper::getRelativeAncestorTags( + *rootShadowNode, *childShadowNode); - if (ancestorList.empty() || ancestorList.size() < 2) { + if (!ancestorTags.has_value()) { return nullptr; } - // ignore the first ancestor as it is the rootShadowNode itself - std::vector ancestorTags; - for (auto it = std::next(ancestorList.begin()); it != ancestorList.end(); - ++it) { - auto& ancestor = *it; - ancestorTags.push_back(ancestor.first.get().getTag()); - } - - jintArray result = env->NewIntArray(static_cast(ancestorTags.size())); + jintArray result = env->NewIntArray(static_cast(ancestorTags->size())); env->SetIntArrayRegion( - result, 0, static_cast(ancestorTags.size()), ancestorTags.data()); + result, 0, static_cast(ancestorTags->size()), ancestorTags->data()); return result; } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FocusOrderingHelper.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FocusOrderingHelper.cpp index 1e4e04c49d9c..a4dc2502e158 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FocusOrderingHelper.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FocusOrderingHelper.cpp @@ -275,4 +275,25 @@ std::optional FocusOrderingHelper::resolveFocusDirection( return std::nullopt; } + +std::optional> FocusOrderingHelper::getRelativeAncestorTags( + const ShadowNode& rootShadowNode, + const ShadowNode& childShadowNode) { + ShadowNode::AncestorList ancestorList = + childShadowNode.getFamily().getAncestors(rootShadowNode); + + if (ancestorList.empty()) { + return std::nullopt; + } + + // ignore the first ancestor as it is the rootShadowNode itself + std::vector ancestorTags; + for (auto it = std::next(ancestorList.begin()); it != ancestorList.end(); + ++it) { + auto& ancestor = *it; + ancestorTags.push_back(ancestor.first.get().getTag()); + } + + return ancestorTags; +} } // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FocusOrderingHelper.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FocusOrderingHelper.h index 802ba1699785..314dc07e90b7 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FocusOrderingHelper.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FocusOrderingHelper.h @@ -5,8 +5,12 @@ * LICENSE file in the root directory of this source tree. */ +#pragma once + #include -#include "FabricUIManagerBinding.h" + +#include +#include namespace facebook::react { @@ -36,5 +40,14 @@ class FocusOrderingHelper { Tag tag); static std::optional resolveFocusDirection(int direction); + + /* + * The tags of the nodes between `rootShadowNode` and `childShadowNode`, + * outermost first: empty for a direct child of `rootShadowNode`, and + * `std::nullopt` when `childShadowNode` does not descend from it. + */ + static std::optional> getRelativeAncestorTags( + const ShadowNode &rootShadowNode, + const ShadowNode &childShadowNode); }; } // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/test/FocusOrderingHelperTest.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/test/FocusOrderingHelperTest.cpp new file mode 100644 index 000000000000..758aa5b2bea5 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/test/FocusOrderingHelperTest.cpp @@ -0,0 +1,90 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include + +#include +#include +#include +#include +#include + +#include +#include + +namespace facebook::react { + +class FocusOrderingHelperTest : public ::testing::Test { + protected: + ComponentBuilder builder_; + std::shared_ptr contentContainer_; + std::shared_ptr directChild_; + std::shared_ptr nestedChild_; + std::shared_ptr detachedNode_; + + FocusOrderingHelperTest() : builder_(simpleComponentBuilder()) { + // clang-format off + auto element = + Element() + .reference(contentContainer_) + .tag(1) + .children({ + Element() + .reference(directChild_) + .tag(2), + Element() + .tag(3) + .children({ + Element() + .tag(4) + .children({ + Element() + .reference(nestedChild_) + .tag(5) + }) + }) + }); + // clang-format on + + builder_.build(element); + builder_.build(Element().reference(detachedNode_).tag(6)); + } +}; + +TEST_F(FocusOrderingHelperTest, testDirectChildHasNoAncestorsBelowTheRoot) { + auto ancestorTags = FocusOrderingHelper::getRelativeAncestorTags( + *contentContainer_, *directChild_); + + ASSERT_TRUE(ancestorTags.has_value()); + EXPECT_TRUE(ancestorTags->empty()); +} + +TEST_F( + FocusOrderingHelperTest, + testNestedChildListsItsAncestorsBelowTheRootOutermostFirst) { + auto ancestorTags = FocusOrderingHelper::getRelativeAncestorTags( + *contentContainer_, *nestedChild_); + + ASSERT_TRUE(ancestorTags.has_value()); + EXPECT_EQ(std::vector({3, 4}), *ancestorTags); +} + +TEST_F(FocusOrderingHelperTest, testNodeOutsideTheRootHasNoAncestorList) { + auto ancestorTags = FocusOrderingHelper::getRelativeAncestorTags( + *contentContainer_, *detachedNode_); + + EXPECT_FALSE(ancestorTags.has_value()); +} + +TEST_F(FocusOrderingHelperTest, testRootHasNoAncestorListRelativeToItself) { + auto ancestorTags = FocusOrderingHelper::getRelativeAncestorTags( + *contentContainer_, *contentContainer_); + + EXPECT_FALSE(ancestorTags.has_value()); +} + +} // namespace facebook::react diff --git a/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js b/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js index ef937f99089c..184be98051e3 100644 --- a/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js +++ b/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js @@ -405,9 +405,10 @@ const examples: Array = [ }, }, { + name: 'removeClippedSubviews', title: ' Remove Clipped Subviews\n', description: - 'When true, offscreen child views (whose overflow value is hidden) are removed from their native backing superview when offscreen.', + 'When true, offscreen child views (whose overflow value is hidden) are removed from their native backing superview when offscreen. The rows are focusable, so Tab on a hardware keyboard should reach every row either way.', render(): React.Node { return ; }, @@ -844,7 +845,9 @@ const RemoveClippedSubviews = () => { style={[styles.scrollView, {height: 200}]} removeClippedSubviews={removeClippedSubviews} nestedScrollEnabled> - {ITEMS.map(createItemRow)} + {ITEMS.map((msg, index) => ( + + ))}