Skip to content

fix: Android hitSlop prop TalkBack selection#54199

Open
coolsoftwaretyler wants to merge 3 commits into
react:mainfrom
coolsoftwaretyler:fix/android-hitslop-talkback
Open

fix: Android hitSlop prop TalkBack selection#54199
coolsoftwaretyler wants to merge 3 commits into
react:mainfrom
coolsoftwaretyler:fix/android-hitslop-talkback

Conversation

@coolsoftwaretyler

Copy link
Copy Markdown

Summary:

Fixes #54187 by intercepting TalkBack hover events inside dispatchGenericMotionEvent and checking if the tap falls inside the child's hitSlop area. If the tap occurs in the hitSlop zone, the method now requests accessibility focus on that child view.

Changelog:

Pick one each for the category and type tags:

[ANDROID] [FIXED] - fix hitSlop prop not providing full hitSlop boundaries to TalkBack

Test Plan:

You can test this in the "Pressable Hit Slop" segment of the rn-tester app on Android.

  1. Run install: yarn install
  2. Open RN tester app: yarn android
  3. Go to "Pressable"
  4. Scroll to "Pressable Hit Slop"
  5. Turn on TalkBack
  6. Tap in the hitSlop area (not on the pressable itself). With the fix, the pressable should be highlighted. before the fix, it would not be, and would need to be tapped directly.

Screen recording of fixed behavior in this branch

fixed.mov

Screen recording of broken behavior on main

broken.mov

@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 18, 2025
@facebook-github-bot facebook-github-bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 18, 2025
Comment on lines +309 to +310
if (ev.isFromSource(android.view.InputDevice.SOURCE_CLASS_POINTER) &&
(ev.action == MotionEvent.ACTION_HOVER_ENTER || ev.action == MotionEvent.ACTION_HOVER_MOVE)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hover actions are not only used by TalkBack, but also by many other platforms (eg VR).

Is there a way to scope this changes more, or ideally, reuse logic from TouchTargetHelper?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sure that makes sense. I will take another look and see what I can find.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the simplest way to do this here was just to check if touch exploration is enabled and keep the rest of the changes, like this: 3550da1.

I looked at TouchTargetHelper and I see there's methods to do hitSlop checking, which we could reuse, but I think we'd have to make them public? If that works, or you had something else in mind, let me know and I'll keep iterating.

@javache javache left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make the change to expose the API - once we have code thats working and not duplicating logic, we can figure out how to reduce visibility changes.

We shouldn't be getting the accessibilitymanager or calling isTouchExplorationEnabled per frame though, as that will cause us to drop frames.

Comment on lines +311 to +312
val accessibilityManager = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager
if (accessibilityManager?.isTouchExplorationEnabled == true &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still pretty expensive to do on a per-input basis (I think it needs to a Binder call)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah that makes sense. I've added some internal state to ReactViewGroup and a TouchExplorationStateChangeListener to manage it.

fix: clean up unncessary bounds checking

fix: use listener instead
@coolsoftwaretyler

Copy link
Copy Markdown
Author

@javache - thanks again for your time reviewing. I exposed the isTouchPointInView method, pulled it in, dropped the normal bounds check (I think that was maybe superfluous), and then set up faster tracking for the touch exploration state. Let me know what you think now. My demo case continues to work as shown in the recordings.

Comment on lines 154 to +159
private var accessibilityStateChangeListener:
AccessibilityManager.AccessibilityStateChangeListener? =
null
private var touchExplorationStateChangeListener:
AccessibilityManager.TouchExplorationStateChangeListener? =
null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both of these should probably live in ReactViewManager or similar - it's wasteful to have every view observe this independently.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @javache - I'd be happy to make this change, but can you provide any more specifics? I just want to respect your time and make sure I understand what you're looking for more clearly before asking for another review.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @javache - any additional thoughts? I'd love to move this forward if possible, would love more details on your feedback. Thanks in advance!

@popsiclelmlm popsiclelmlm left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed one lifecycle issue in the current version:

onAttachedToWindow() only registers the TouchExplorationStateChangeListener when touchExplorationStateChangeListener == null, but onDetachedFromWindow() removes the listener without clearing that field. If the same ReactViewGroup instance is detached and later reattached, it will skip re-registering the listener because the field is still non-null, leaving isTouchExplorationEnabled stale after reattach.

A small fix would be to set touchExplorationStateChangeListener = null after removal, or to separate “listener object exists” from “listener is currently registered”. A focused lifecycle test for detach/reattach would be helpful too, since this PR relies on the cached touch-exploration state staying current.

@coolsoftwaretyler

Copy link
Copy Markdown
Author

Thanks, @popsiclelmlm. I can push up a commit soon, although I think my fork is having some problems (or maybe GitHub is?). I'll try again sometime this week. Should be a small fix.

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. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hitSlop does not surface to Android TalkBack

4 participants