Skip to content

Follow Android's tab order in a ScrollView's clipped focus search - #59074

Draft
YevheniiKotyrlo wants to merge 2 commits into
react:mainfrom
YevheniiKotyrlo:fix/android-focus-search-tab-order
Draft

YevheniiKotyrlo wants to merge 2 commits into
react:mainfrom
YevheniiKotyrlo:fix/android-focus-search-tab-order

Conversation

@YevheniiKotyrlo

Copy link
Copy Markdown
Contributor

Summary:

When the last focusable view of a ScrollView in Android's tab order has another focusable below it or to its right, Tab moves back to that view instead of leaving the scroll view, and the two alternate for as long as Tab is pressed.

ReactScrollView.focusSearch hands FOCUS_FORWARD and FOCUS_BACKWARD to the shadow-tree search whenever the platform's next view lies outside the scroll view, so that clipped children can be reached. For those two directions FocusOrderingHelper took any focusable below or to the right of the focused one, which is not Android's order: ViewGroup.addFocusables sorts each parent's children into rows by top edge and then by start edge, and lists a parent before the views it hosts. A card whose action sits below its title, with a dismiss button at its top end, is ordered action then dismiss by Android; from dismiss, the search found the action below it.

FocusOrderingHelper::collectFocusablesInTabOrder now builds Android's order over the shadow tree: it takes each parent's children the way the mounting layer flattens them (sliceChildShadowNodeViewPairs) and sorts them like FocusFinder.FocusSorter. findNextInTabOrder takes the neighbour in that list, or nothing past either end, and then focusSearch keeps the platform's answer. The geometric rules for the two relative directions go; the four absolute directions are unchanged. RNTester gains a ScrollView keyboard focus order example.

#59052 keeps no-hide-descendants subtrees out of the directional search; whichever of the two lands second, I'll carry that rule into the tab order walk.

Changelog:

[ANDROID] [FIXED] - Tab no longer alternates between two views at the end of a ScrollView instead of leaving it

Test Plan:

  • New gtest react/fabric/test/FocusOrderingHelperTabOrderTest.cpp: rows by top and then left edge, right-to-left rows, a row that grows while views overlap it, a view hosting its children ordered as one block, a flattened view's children joining its parent's rows, a focusable parent before its children, the card above, and both ends of the order. The repository's CMake build has no target for that directory, so I added one to ReactAndroid's x86_64 CMake build, linked against libreactnative.so, and ran it on an Android 16 emulator. With findNextInTabOrder answering by main's isCandidate and isBetterCandidate, the card case fails: forward from dismiss gives the action. With this change all eight pass.
  • RNTester, ScrollView → Keyboard focus order, Tab from Action on an Android 16 emulator: on main focus goes Dismiss, Action, Dismiss, Action, Dismiss; with this change it goes Dismiss, then the button after the scroll view, then the bottom navigation. Two rounds per build gave the same sequence. Images in the comment below.
  • yarn flow-check passes; ESLint and Prettier are clean on the example and clang-format on the changed C++ files; the RNTester debug build compiles the native change.

ReactScrollView.focusSearch hands FOCUS_FORWARD and FOCUS_BACKWARD to
the shadow-tree search whenever the platform's next view lies outside
the scroll view. That search took anything below or to the right as the
next view, which is not Android's order: ViewGroup.addFocusables sorts
each parent's children into rows by top edge, then by start edge, and
walks the tree depth first. When the last view in Android's order had
another focusable below or to its right, Tab went back to that view and
the two alternated instead of leaving the scroll view.

FocusOrderingHelper now lists the focusable views in Android's tab
order, following the mounting layer's view flattening, and forward and
backward take the neighbour in that list, or nothing past either end.
RNTester gains a keyboard focus order example for ScrollView.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 14, 2026
@YevheniiKotyrlo

Copy link
Copy Markdown
Contributor Author

Device evidence — before / after

UNFIXED — Tab from Dismiss returns to Action, and every further Tab alternates between the two.

FIXED — Tab from Dismiss leaves the scroll view for the button below it.

before — main at 892ea5e45d after — with this PR

RNTester debug builds of main at 892ea5e45d with the new example, without and with this change, on an Android 16 (API 36) emulator, 1140×2400 at density 480. ScrollView → Keyboard focus order: Tab to Action, then twice more, the first going to Dismiss. Before: focus is back on Action, which takes the darker focus fill, and every further Tab alternates between the two. After: focus has left the scroll view for the button below it. A second round gave the same frames.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant