Add fabric implementation to find next focusable view (#50196)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/50196

Currently when `removeClippedSubviews` is enabled on Android keyboard navigation breaks and we can never focus the elements that are clipped. iOS has a similar issue but not as drastic, it only happens when elements on the FlatList have a lot of margin between them.

This algorithm aims to find the next focusable view and return it to native so that we can prevent the clipping of the view on the view clipping algorithm and hence fix keyboard navigation. For more information see D71324219

Fabric algorithm to find the next focusable view given:

`parentTag`: Top most relevant parent of the focused view
`focusedTag`: Tag of the currently focused view
`direction`: Direction in which focus is moving

Changelog: [Internal]

Reviewed By: joevilches

Differential Revision: D71558965

fbshipit-source-id: 1a13c82d067834337e7f7936860780f467c9a15d
This commit is contained in:
Jorge Cabiedes Acosta
2025-04-14 18:08:36 -07:00
committed by Facebook GitHub Bot
parent 96e49330f8
commit c8a9b9d342
5 changed files with 281 additions and 0 deletions
@@ -55,6 +55,8 @@ internal class FabricUIManagerBinding : HybridClassBase() {
isMountable: Boolean
)
external fun findNextFocusableElement(parentTag: Int, focusedTag: Int, direction: Int): Int
external fun stopSurface(surfaceId: Int)
external fun stopSurfaceWithSurfaceHandler(surfaceHandler: SurfaceHandlerBinding)
@@ -11,6 +11,7 @@
#include "ComponentFactory.h"
#include "EventBeatManager.h"
#include "FabricMountingManager.h"
#include "FocusOrderingHelper.h"
#include <cxxreact/TraceSection.h>
#include <fbjni/fbjni.h>
@@ -195,6 +196,54 @@ void FabricUIManagerBinding::startSurface(
}
}
jint FabricUIManagerBinding::findNextFocusableElement(
jint parentTag,
jint focusedTag,
jint direction) {
ShadowNode::Shared nextNode;
std::optional<FocusDirection> focusDirection =
FocusOrderingHelper::resolveFocusDirection(direction);
if (!focusDirection.has_value()) {
return -1;
}
std::shared_ptr<UIManager> uimanager = getScheduler()->getUIManager();
ShadowNode::Shared parentShadowNode =
uimanager->findShadowNodeByTag_DEPRECATED(parentTag);
ShadowNode::Shared focusedShadowNode =
FocusOrderingHelper::findShadowNodeByTagRecursively(
parentShadowNode, focusedTag);
LayoutMetrics childLayoutMetrics = uimanager->getRelativeLayoutMetrics(
*focusedShadowNode, parentShadowNode.get(), {.includeTransform = true});
Rect sourceRect = childLayoutMetrics.frame;
/*
* Traverse the tree recursively to find the next focusable element in the
* given direction
*/
std::optional<Rect> nextRect = std::nullopt;
FocusOrderingHelper::traverseAndUpdateNextFocusableElement(
parentShadowNode,
focusedShadowNode,
parentShadowNode,
focusDirection.value(),
*uimanager,
sourceRect,
nextRect,
nextNode);
if (nextNode == nullptr) {
return -1;
}
return nextNode->getTag();
}
// Used by non-bridgeless+Fabric
void FabricUIManagerBinding::startSurfaceWithConstraints(
jint surfaceId,
@@ -667,6 +716,9 @@ void FabricUIManagerBinding::registerNatives() {
makeNativeMethod(
"stopSurfaceWithSurfaceHandler",
FabricUIManagerBinding::stopSurfaceWithSurfaceHandler),
makeNativeMethod(
"findNextFocusableElement",
FabricUIManagerBinding::findNextFocusableElement),
});
}
@@ -132,6 +132,9 @@ class FabricUIManagerBinding : public jni::HybridClass<FabricUIManagerBinding>,
void reportMount(SurfaceId surfaceId);
jint
findNextFocusableElement(jint parentTag, jint focusedTag, jint direction);
void uninstallFabricUIManager();
// Private member variables
@@ -0,0 +1,186 @@
/*
* 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 "FocusOrderingHelper.h"
#include <android/log.h>
#include <react/renderer/uimanager/UIManager.h>
namespace facebook::react {
int majorAxisDistanceRaw(
FocusDirection focusDirection,
Rect source,
Rect dest) {
switch (focusDirection) {
case FocusDirection::FocusLeft:
return static_cast<int>(
source.origin.x - (dest.origin.x + dest.size.width));
case FocusDirection::FocusRight:
return static_cast<int>(
dest.origin.x - (source.origin.x + source.size.width));
case FocusDirection::FocusUp:
return static_cast<int>(
source.origin.y - (dest.origin.y + dest.size.height));
case FocusDirection::FocusDown:
return static_cast<int>(
dest.origin.y - (source.origin.y + source.size.height));
}
}
int majorAxisDistance(FocusDirection focusDirection, Rect source, Rect dest) {
return std::max(0, majorAxisDistanceRaw(focusDirection, source, dest));
}
int minorAxisDistance(FocusDirection direction, Rect source, Rect dest) {
switch (direction) {
case FocusDirection::FocusLeft:
case FocusDirection::FocusRight:
// the distance between the center verticals
return static_cast<int>(abs((source.getMidY() - dest.getMidY())));
case FocusDirection::FocusUp:
case FocusDirection::FocusDown:
// the distance between the center horizontals
return static_cast<int>(abs((source.getMidX() - dest.getMidX())));
}
}
// 13 is a magic number that comes from Android's implementation. We opt to use
// this to get the same focus ordering as Android. See:
// https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/view/FocusFinder.java;l=547
int getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance) {
return 13 * majorAxisDistance * majorAxisDistance +
minorAxisDistance * minorAxisDistance;
}
// Make sure dest rect is actually on the direction of focus
bool isCandidate(Rect source, Rect dest, FocusDirection focusDirection) {
switch (focusDirection) {
case FocusDirection::FocusLeft:
return ((source.origin.x + source.size.width) >
(dest.origin.x + dest.size.width) ||
source.origin.x >= (dest.origin.x + dest.size.width)) &&
source.origin.x > dest.origin.x;
case FocusDirection::FocusRight:
return (source.origin.x < dest.origin.x ||
(source.origin.x + source.size.width) <= dest.origin.x) &&
(source.origin.x + source.size.width) <
(dest.origin.x + dest.size.width);
case FocusDirection::FocusUp:
return ((source.origin.y + source.size.height) >
(dest.origin.y + dest.size.height) ||
source.origin.y >= (dest.origin.y + dest.size.height)) &&
source.origin.y > dest.origin.y;
case FocusDirection::FocusDown:
return (source.origin.y < dest.origin.y ||
(source.origin.y + source.size.height) <= dest.origin.y) &&
((source.origin.y + source.size.height) <
(dest.origin.y + dest.size.height));
}
}
bool isBetterCandidate(
FocusDirection focusDirection,
Rect source,
Rect current,
Rect candidate) {
if (!isCandidate(source, candidate, focusDirection)) {
return false;
}
int candidateWeightedDistance = getWeightedDistanceFor(
majorAxisDistance(focusDirection, source, candidate),
minorAxisDistance(focusDirection, source, candidate));
int currentWeightedDistance = getWeightedDistanceFor(
majorAxisDistance(focusDirection, source, current),
minorAxisDistance(focusDirection, source, current));
return candidateWeightedDistance < currentWeightedDistance;
}
void FocusOrderingHelper::traverseAndUpdateNextFocusableElement(
const ShadowNode::Shared& parentShadowNode,
const ShadowNode::Shared& focusedShadowNode,
const ShadowNode::Shared& currNode,
FocusDirection focusDirection,
const UIManager& uimanager,
Rect sourceRect,
std::optional<Rect>& nextRect,
ShadowNode::Shared& nextNode) {
const auto* props =
dynamic_cast<const ViewProps*>(currNode->getProps().get());
// We only care about focusable elements since only they can be both
// focused and present in the hierarchy
if (currNode->getTraits().check(ShadowNodeTraits::Trait::KeyboardFocusable) ||
(props != nullptr &&
(props->focusable || props->accessible || props->hasTVPreferredFocus))) {
LayoutMetrics nodeLayoutMetrics = uimanager.getRelativeLayoutMetrics(
*currNode, parentShadowNode.get(), {.includeTransform = true});
if (nextRect == std::nullopt &&
isCandidate(sourceRect, nodeLayoutMetrics.frame, focusDirection)) {
nextNode = currNode;
nextRect = nodeLayoutMetrics.frame;
} else if (
nextRect != std::nullopt &&
isBetterCandidate(
focusDirection,
sourceRect,
nextRect.value(),
nodeLayoutMetrics.frame)) {
nextNode = currNode;
nextRect = nodeLayoutMetrics.frame;
}
}
for (auto& child : currNode->getChildren()) {
if (child->getTraits().check(ShadowNodeTraits::Trait::RootNodeKind)) {
continue;
}
traverseAndUpdateNextFocusableElement(
parentShadowNode,
focusedShadowNode,
child,
focusDirection,
uimanager,
sourceRect,
nextRect,
nextNode);
};
};
ShadowNode::Shared FocusOrderingHelper::findShadowNodeByTagRecursively(
const ShadowNode::Shared& parentShadowNode,
Tag tag) {
if (parentShadowNode->getTag() == tag) {
return parentShadowNode;
}
for (auto& shadowNode : parentShadowNode->getChildren()) {
if (auto result = findShadowNodeByTagRecursively(shadowNode, tag)) {
return result;
}
}
return nullptr;
}
std::optional<FocusDirection> FocusOrderingHelper::resolveFocusDirection(
int direction) {
switch (static_cast<FocusDirection>(direction)) {
case FocusDirection::FocusDown:
case FocusDirection::FocusUp:
case FocusDirection::FocusRight:
case FocusDirection::FocusLeft:
return static_cast<FocusDirection>(direction);
}
return std::nullopt;
}
} // namespace facebook::react
@@ -0,0 +1,38 @@
/*
* 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 <react/renderer/uimanager/UIManager.h>
#include "FabricUIManagerBinding.h"
namespace facebook::react {
enum class FocusDirection {
FocusDown = 0,
FocusUp = 1,
FocusRight = 2,
FocusLeft = 3,
};
class FocusOrderingHelper {
public:
static void traverseAndUpdateNextFocusableElement(
const ShadowNode::Shared& parentShadowNode,
const ShadowNode::Shared& focusedShadowNode,
const ShadowNode::Shared& currNode,
FocusDirection focusDirection,
const UIManager& uimanager,
Rect sourceRect,
std::optional<Rect>& nextRect,
ShadowNode::Shared& nextNode);
static ShadowNode::Shared findShadowNodeByTagRecursively(
const ShadowNode::Shared& parentShadowNode,
Tag tag);
static std::optional<FocusDirection> resolveFocusDirection(int direction);
};
} // namespace facebook::react