Repository navigation
Keep focusable={false} Android views out of keyboard navigation - #59048
Open
YevheniiKotyrlo wants to merge 2 commits into
Open
YevheniiKotyrlo wants to merge 2 commits into
YevheniiKotyrlo wants to merge 2 commits into
Conversation
An accessible view keeps isFocusable for accessibility services, so
setFocusable(false) left it in Tab and arrow-key navigation: every
Pressable is accessible, and tabIndex={-1} or focusable={false} on one
changed nothing about where a hardware keyboard moved focus.
ReactViewGroup now records the prop and skips the view in addFocusables
and requestFocus, the two ways keyboard navigation reaches it, while its
descendants, accessibility services and requestFocusFromJS still do.
Contributor
Author
When the next focus lies outside a scroll view, ReactScrollView and its
horizontal and nested siblings ask the shadow tree for the next
focusable element inside it, and that search offers every focusable or
accessible node. A view focusable={false} keeps out of keyboard
navigation is still accessible, so the search handed it back, the view
refused focus and Tab stopped moving. findNextFocusableView now moves
on from a candidate whose addFocusables adds nothing, and ends at a
candidate it already offered, since forward candidates can lead back to
each other. ReactViewGroup's addFocusables and requestFocus overrides
are added to ReactAndroid.api.
YevheniiKotyrlo
marked this pull request as ready for review
October 14, 2026 13:41
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,
focusable={false}— andtabIndex={-1}, whichViewturns intofocusable={false}— does not take a view out of hardware-keyboard navigation when the view is alsoaccessible.Pressable,TouchableOpacityandTouchableHighlightare accessible by default, so on any of them Tab, Shift+Tab and the arrow keys still land on the view.setAccessiblesetsisFocusableso accessibility services treat the view as one element, andsetFocusable(false)deliberately leaves that flag alone. Android's keyboard navigation reads the same flag:FocusFindercollects candidates throughaddFocusables, and with nothing focusedViewRootImplcallsrestoreDefaultFocus, which reaches a view throughrequestFocus.ReactViewGroupnow records the prop and honours it at those two entry points:addFocusablesleaves the view out and keeps its descendants, andrequestFocushands the request to its descendants.isFocusableis untouched, so accessibility services,ref.focus()(which already callssuper.requestFocus) and the click wiring behave as before. A view that holds focus stays a candidate:FocusFinderloops to the first candidate when the focused view is missing from the list, so Tab from a view focused withref.focus()would otherwise jump to the start of the window.Inside a scroll view one more search runs. When the next focus lies outside it,
ReactScrollViewand its horizontal and nested siblings ask the shadow tree for the next focusable element (enableCustomFocusSearchOnClippedElementsAndroid), and that search offers every focusable or accessible node. It would hand back an excluded view nested in the scroll view's content, the view would refuse focus, and Tab would stop moving, soReactScrollViewHelper.findNextFocusableViewnow moves on from a candidate whoseaddFocusablesadds nothing. Forward candidates can lead back to each other, so it also ends at a candidate it has already offered. The two overrides are added toReactAndroid.api. This overlaps #59052, which also overridesReactViewGroup.addFocusables; whichever lands second combines the two bodies in one function.I searched the tracker for
focusable={false}andtabIndexwith keyboard focus and found no existing report or PR.Changelog:
[ANDROID] [FIXED] -
focusable={false}andtabIndex={-1}take an accessible view out of hardware-keyboard navigationTest Plan:
./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest --tests "com.facebook.react.views.view.ReactViewGroupTest" --tests "com.facebook.react.views.scroll.ReactScrollViewHelperFocusSearchTest" -Preact.internal.useHermesStable=true: onmainsix of the twelve new tests fail —addFocusablesreturns the excluded view (Expecting actual: [View, ReactViewGroup, View] to contain exactly (and in same order): [View, View], and the same with a descendant),requestFocus(FOCUS_DOWN)gives it focus, and the scroll view search hands back the excluded candidate where the next one or none belongs, also when the candidates lead back to the first box. With the fix all 29 tests in the two classes pass, including the ones forref.focus(), Tab and Shift+Tab from a focused excluded view,focusable={true}restoring the view, recycling, and a candidate that takes focus itself or through a descendant../gradlew :packages:react-native:ReactAndroid:testDebugUnitTest :packages:react-native:ReactAndroid:compileReleaseKotlin :packages:react-native:ReactAndroid:compileReleaseJavaWithJavac -PenableWarningsAsErrors=true -Preact.internal.useHermesStable=true: 725 tests in 104 suites, 0 failures, and the release sources compile.node ./scripts/format-kotlin.js --checkpasses.mainatb8ec87e457passes with-PenableWarningsAsErrors=true; the rest ofbuild_androidwas not run. With only the first commit, the scroll view scene below stops Tab on its first box.tabIndex={-1}pressable without the change and passes over it with the change, in a plain view and in a scroll view.ReactViewGroupchange on 0.86.3, in an app on a Galaxy S23 (Android 14), with Tab sent as key events: Tab passes over pressables markedtabIndex={-1}, where it landed on them before.