Repository navigation
Conversation
focusSearch kept Android's result only if findViewById(nextFocus.id) found it, so focusable views without an id, such as the AndroidComposeView inside a ComposeView, were replaced by the shadow tree search and skipped by Tab and Shift+Tab. Check whether the view is inside the scroll view by walking its parents instead.
|
Hi @LLUhlin! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
Summary:
On Android, a hardware keyboard's Tab and Shift+Tab skip focusable views without an id inside a
ScrollView. We hit this with a Jetpack ComposeHostfrom@expo/ui(aSingleChoiceSegmentedButtonRow): Tab from the view above, or Shift+Tab from the view below, jumps past it.With
enableCustomFocusSearchOnClippedElementsAndroidon (the default),focusSearchinReactScrollView,ReactHorizontalScrollViewandReactNestedScrollViewkeeps Android's result only iffindViewById(nextFocus.id)finds it. The intent, per the comment inReactHorizontalScrollView, is to check that the next focus is inside the scroll view. An id lookup only approximates that:View.NO_ID) is never found, becausefindViewById(NO_ID)returnsnull. The search then falls back tofindNextFocusableView, which only knows about the shadow tree and returns the next React view instead. The view Android focuses for Compose content is theAndroidComposeViewthatComposeViewadds as its child, and it has no id.This change replaces the lookup with a walk up the parents of
nextFocus(ReactScrollViewHelper.isSelfOrDescendant). Views inside the scroll view keep Android's result whether or not they have an id. Views outside it, and views detached byremoveClippedSubviews, still go throughfindNextFocusableViewas before. The walk is also cheaper thanfindViewById, which searches the whole subtree.ReactNestedScrollView.ktwas regenerated withgenerate-nested-scroll-view.js.Related: expo/expo#51207 works around the same problem from the Expo side by giving the
AndroidComposeViewan id.Changelog:
[ANDROID] [FIXED] - Fix keyboard Tab and Shift+Tab skipping focusable views without an id, such as Jetpack Compose content, inside a ScrollView
Test Plan:
Unit tests: added
ReactScrollViewHelperFocusTest. It covers the host itself, descendants with and without an id, a view outside the host whose id matches a descendant, and a detached view.All 14 tests in
com.facebook.react.views.scrollpass.node ./scripts/format-kotlin.js --checkandnode generate-nested-scroll-view.js --verifypass.On device: tested on React Native 0.86.2 (New Architecture,
@expo/ui57.0.14) on a physical Android phone with a Bluetooth keyboard. The same change, in the 0.86.2 Java sources, was compiled into thereact-android0.86.2 AAR, and the Expo workaround was removed, so this change was the only fix in the build. Screen: aScrollViewwith a React view, an@expo/uiHostwith aSingleChoiceSegmentedButtonRow, then another React view.