Skip to content

Keep focusable={false} Android text out of keyboard navigation - #59139

Draft
YevheniiKotyrlo wants to merge 3 commits into
react:mainfrom
YevheniiKotyrlo:fix/android-text-focusable-keyboard-focus
Draft

YevheniiKotyrlo wants to merge 3 commits into
react:mainfrom
YevheniiKotyrlo:fix/android-text-focusable-keyboard-focus

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

On Android, a Text with onPress or onLongPress is accessible unless told otherwise, and setAccessible makes it focusable, so Tab, Shift+Tab and the arrow keys of a hardware keyboard stop on it. Text has no focusable prop to take it out, so a pressable label above an input is always an extra Tab stop. #34737 asks for keyboard focus to be specified separately from screen-reader focus.

This gives Text the focusable prop with the meaning #59048 gives focusable={false} on View. ReactTextView and PreparedLayoutTextView record it and skip an excluded text at the two points keyboard navigation reaches it — addFocusables, where an inline view inside the text stays a candidate, and requestFocus — while isFocusable stays with accessible, so accessibility services still treat the text as one element and activate it. A text that holds focus stays a candidate, so focus search can move on from it. In touch mode requestFocus still succeeds, because a selectable text takes focus from the long press that starts a selection. The base view config already sends focusable to the native text component; TextProps and the legacy types now declare it.

This is stacked on #59048, whose change to ReactScrollViewHelper.findNextFocusableView passes over a scroll-view candidate that takes no keyboard focus; only the last commit is new.

Changelog:

[ANDROID] [ADDED] - Text accepts focusable={false}, which takes an accessible text out of hardware-keyboard navigation while accessibility services still reach it

Test Plan:

  • ./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest --tests "com.facebook.react.views.text.ReactTextViewKeyboardFocusTest" --tests "com.facebook.react.views.text.PreparedLayoutTextViewKeyboardFocusTest" -Preact.internal.useHermesStable=true: with the text view sources from the base, 5 of the 14 new tests fail — addFocusables returns the excluded text (Expecting actual: [View, ReactTextView, View] to contain exactly (and in same order): [View, View], the same for PreparedLayoutTextView and beside an inline view) and requestFocus(FOCUS_DOWN) gives it focus. With the change all 14 pass, including Tab and Shift+Tab from a focused excluded text, focusable={true} and a removed prop returning the text, recycling, and a touch still focusing an excluded selectable text.
  • ./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest :packages:react-native:ReactAndroid:compileReleaseKotlin :packages:react-native:ReactAndroid:compileReleaseJavaWithJavac -PenableWarningsAsErrors=true -Preact.internal.useHermesStable=true: 782 tests, 0 failures, and the release sources compile.
  • yarn flow-check, yarn lint, yarn test-typescript-legacy, yarn build-types and yarn test-generated-typescript pass, ReactNativeApi.d.ts is regenerated, and ktfmt and google-java-format pass on the changed files.
  • RNTester on an Android 16 emulator, before and after: the second Tab lands on a pressable Text marked focusable={false} without the change and passes over it with the change, in a plain view and in a scroll view.
  • The same change on 0.86.3, in an app on a Mi MIX 3 (Android 10) driven by Tab key events: Tab moves from field to field in a form whose labels are pressable Text, where it stopped on each label before.

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.
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.
A Text with onPress or onLongPress is accessible, so setAccessible made
it focusable, and Tab, Shift+Tab and the arrow keys of a hardware
keyboard stopped on it. Text had no focusable prop to take it out, so a
pressable label between two inputs was always an extra Tab stop.

The text view managers now take focusable, and ReactTextView and
PreparedLayoutTextView skip an excluded text in addFocusables and
requestFocus while isFocusable stays with accessible, so accessibility
services still reach it. A text that holds focus stays a candidate, an
inline view inside one stays reachable, and a touch still focuses a
selectable text, which text selection needs. TextProps and the legacy
Text types declare the prop; the base view config already sends it.
@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 16, 2026
@YevheniiKotyrlo

Copy link
Copy Markdown
Contributor Author

Device evidence — before / after

UNFIXED — from touch mode, the first Tab focuses the First box and the second lands on the pressable Text marked focusable={false}.

FIXED — the second Tab passes over the text to the Last box.

before — #59048's head, b969e83933 after — with this PR

Screen recordings of RNTester debug builds of #59048's head and of this PR's head on an Android 16 (API 36) emulator, 1140×2400 at density 480, with Tab pressed twice from touch mode. The scene is two Pressables with a Text between them that has onPress and focusable={false}, above a line naming the one that holds focus; in the second row the first Pressable and the text sit in an outlined ScrollView, the text inside a plain View.

@github-actions

Copy link
Copy Markdown

Warning

JavaScript API change detected

This PR commits an update to ReactNativeApi.d.ts, indicating a change to React Native's public JavaScript API.

  • Please include a clear changelog message.
  • This change will be subject to additional review.

This change was flagged as: POTENTIALLY_BREAKING

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