Repository navigation
Keep hidden Android view subtrees out of keyboard navigation - #59052
Open
YevheniiKotyrlo wants to merge 2 commits into
Open
YevheniiKotyrlo wants to merge 2 commits into
YevheniiKotyrlo wants to merge 2 commits into
Conversation
Contributor
Author
A View with importantForAccessibility="no-hide-descendants", which is what aria-hidden becomes, hid its descendants from TalkBack while Tab, the arrow keys and a D-pad still landed on them. ReactViewGroup now leaves those descendants out of the focus search and out of focus that enters through the view, and keyboard focus sitting on one moves off when the view hides it. A tap or a focus() call still focuses them, and the view's own descendantFocusability is untouched. The shadow tree search a scroll view falls back to when focus leaves it skips those descendants too, so Tab past a scroll view's last control no longer picks a hidden one and stops there.
A ScrollView, a horizontal ScrollView, DrawerLayoutAndroid or RefreshControl with importantForAccessibility="no-hide-descendants" left its content in keyboard navigation, because only ReactViewGroup filtered the descendants it hides. The filtering moves into HiddenSubtreeFocusHelper, which every container React Native renders calls from addFocusables and onRequestFocusInDescendants, reading importantForAccessibility rather than a copy of it, and BaseViewManager moves keyboard focus off a subtree as it hides it, whatever the view type. The new public overrides are added to ReactAndroid.api.
YevheniiKotyrlo
force-pushed
the
fix/android-hidden-subtree-keyboard-focus
branch
from
October 14, 2026 09:17
5a605ee to
27b042e
Compare
YevheniiKotyrlo
marked this pull request as ready for review
October 14, 2026 10:39
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.






Summary:
On Android,
aria-hidden={true}— andimportantForAccessibility="no-hide-descendants", whichViewturns it into — hides a view's descendants from TalkBack, while Tab, Shift+Tab, the arrow keys and a D-pad still land on any focusable descendant. A keyboard user ends up on a control that assistive technology treats as absent and that the screen usually shows as covered or inactive: a modal's background, a stacked notification, an inactive screen.Keyboard navigation reaches a subtree in three places, and none of them reads
importantForAccessibility.FocusFindercollects candidates throughaddFocusables. With nothing focused,ViewRootImplcallsrestoreDefaultFocus, which enters a subtree throughonRequestFocusInDescendants. And when the next candidate lies outside a scroll view,ReactScrollView.focusSearchand its horizontal and nested siblings ask the shadow tree for the next focusable element (enableCustomFocusSearchOnClippedElementsAndroid), whereFocusOrderingHelper::traverseAndUpdateNextFocusableElementdescends into every child.Every container React Native renders on Android —
ReactViewGroup, the vertical, horizontal and nested scroll views,DrawerLayoutAndroidandRefreshControl— now leaves the descendants it hides out ofaddFocusablesandonRequestFocusInDescendants, through one internalHiddenSubtreeFocusHelperthat reads the view'simportantForAccessibility. A descendant that holds focus stays among the candidates, so focus search moves on from it instead of looping to the first candidate, and the container itself keeps the place the platform gives it.BaseViewManagermoves keyboard focus off a descendant when a view starts hiding it; in touch mode the focus stays, so a text field someone is typing in keeps its keyboard. A tap orref.focus()still focuses a hidden descendant, anddescendantFocusabilityis left as the app or a subclass set it.FocusOrderingHelperstops before a hiding node's descendants, so the shadow-tree search agrees with the view tree. The new public overrides are added toReactAndroid.api.ScrollView,DrawerLayoutAndroidandRefreshControltakeimportantForAccessibilitydirectly; unlikeView, they do not turnaria-hiddeninto it. This overlaps #59048, which also overridesReactViewGroup.addFocusables; whichever lands second combines the two bodies in one function.The nearest report I found is #30850 (closed), which expects
no-hide-descendantsto keep descendants from taking focus, on 0.63 and for screen-reader focus. I found no PR.Changelog:
[ANDROID] [FIXED] -
aria-hiddenandimportantForAccessibility="no-hide-descendants"keep a view's descendants out of hardware-keyboard navigationTest Plan:
./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest --tests "com.facebook.react.views.view.ReactViewGroupTest" --tests "com.facebook.react.uimanager.HiddenSubtreeKeyboardFocusTest" -Preact.internal.useHermesStable=true: onmain, 8 of the 14 new tests fail. InReactViewGroupTest, five: the hidden descendant is in theaddFocusableslist,requestFocus(FOCUS_DOWN)on the root gives it focus, it keeps keyboard focus after its parent hides it, while one keeps touch focus its hidden siblings are still candidates, and a focusable hiding view comes with its descendants.HiddenSubtreeKeyboardFocusTestruns each scroll view, the drawer and the refresh layout through the same scene, and three of its four tests fail: the hidden content is a candidate, takes default focus, and keeps keyboard focus as the container hides it. The six that pass either way hold what stays the same: hidden content returns once shown again, a directrequestFocus()still focuses it, touch focus stays, a hiding view keeps focus it holds itself, anddescendantFocusabilityis untouched. With the change all 31 tests in the two classes pass../gradlew :packages:react-native:ReactAndroid:testDebugUnitTest :packages:react-native:ReactAndroid:compileReleaseKotlin :packages:react-native:ReactAndroid:compileReleaseJavaWithJavac -PenableWarningsAsErrors=true -Preact.internal.useHermesStable=true: 735 tests in 104 suites, 0 failures, and the release sources compile.node ./scripts/format-kotlin.js --check,node ./scripts/format-java.js --check,node ./scripts/clang-format.js --checkon the changed C++ file, andnode generate-nested-scroll-view.js --verifypass.mainatb8ec87e457passes with-PenableWarningsAsErrors=true. The rest ofbuild_androidand the C++ API snapshot check were not run; the change declares nothing new in C++.FocusOrderingHelperhas no C++ test target, so the scroll view scene below is what exercises that half.aria-hiddenview, anaria-hiddenview in a scroll view, and a scroll view withimportantForAccessibility="no-hide-descendants".ReactViewGrouphalf and theFocusOrderingHelperchange on 0.86.3, in an app on a Galaxy S23 (Android 14), with Tab sent as key events: Tab passes over the controls inaria-hiddennotifications stacked behind the front one, where it landed on them before.