Skip to content

fix(android): a long-press drag selection is no longer stolen by a scrolling parent - #1012

Merged
hryhoriiK97 merged 3 commits into
software-mansion:mainfrom
voida6:fix/android-selection-drag-parent-intercept
Oct 18, 2026
Merged

hryhoriiK97 merged 3 commits into
software-mansion:mainfrom
voida6:fix/android-selection-drag-parent-intercept

Conversation

@voida6

@voida6 voida6 commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

What/Why?

On Android, extending a text selection by long-pressing a word and dragging often goes wrong when the text sits inside a scrolling parent (a ScrollView or a FlatList of messages). The magnifier shows and the selection grows, then the gesture is cut off: on finger-up there are no selection handles and no selection toolbar, and the selection collapses or needs an extra tap before Copy shows. Double-tapping a word works, because nothing moves.

Cause:

  1. Once the finger moves during a long-press selection, the framework Editor calls requestDisallowInterceptTouchEvent(true) on the TextView's parents and relies on that until ACTION_UP.
  2. reallowParentInterceptIfLinkReleased() (added in fix(android): prevent RNGH Pressable from stealing link tap gestures #477) runs after every non-ACTION_DOWN event and calls requestDisallowInterceptTouchEvent(false) whenever no link is pressed. So on the next ACTION_MOVE it undoes the Editor's request.
  3. The scrolling parent then intercepts the drag, the TextView gets ACTION_CANCEL, and the Editor never starts the selection action mode.

The re-allow is only there to undo the request cancelJSTouchForLinkTap() makes for a link press. This change records when cancelJSTouchForLinkTap() made that request (LinkLongPressMovementMethod.disallowsParentIntercept), and reallowParentInterceptIfLinkReleased() only undoes it in that case. Link presses behave as before: the request is made on ACTION_DOWN over a link and released when the link touch ends or moves past the slop. For any other gesture the call was redundant anyway, since a ViewGroup clears the disallow flag itself on the next ACTION_DOWN. A normal swipe on text still scrolls the parent, because nothing requests disallow for it.

Only react-native-enriched-markdown has this function; enriched-markdown-android does not re-allow and is unchanged.

Testing

Release build of an app on react-native-enriched-markdown (T3 Code) with this patch, on a Galaxy S25 Ultra (Android 16, One UI). Assistant message text inside the app's scrolling message list, no links under the finger. Two sideways long-press drags, one downward long-press drag, then a double-tap. Touches are shown as white dots.

  • Before: drags were cut off mid-selection, with no handles or toolbar on finger-up.
  • After: each of 4 long-press drags ended with handles and the Copy toolbar as soon as the finger lifted. A debug log on the TextView recorded ACTION_UP with hasSelection() == true for each of them and no ACTION_CANCEL mid-drag. Plain swipes on the same text still scrolled the list (they logged ACTION_CANCEL with no selection, as expected for a parent scroll). On the same build, a link tap, a link long-press and a long-press that slides off a link behaved as before.

Before:

before-drag-selection.mp4

After:

after-drag-selection.mp4

I did not add a unit test: the React Native package has no unit test setup, and the standalone Android package does not have this function. ktlint 1.8.0 (the version the build uses) passes on both changed files.

PR Checklist

  • Code compiles and runs on iOS
  • Code compiles and runs on Android
  • Updated documentation/README if applicable
  • Ran example app to verify changes
  • E2E tests are passing
  • Required E2E tests have been added (if applicable)

iOS is unchanged. I verified in a downstream app (T3 Code) instead of the example app, and did not run the Maestro suite. No docs change.

…rolling parent

When the finger moves during a long-press text selection, the framework Editor
calls requestDisallowInterceptTouchEvent(true) on the TextView's parents and
keeps that until ACTION_UP. reallowParentInterceptIfLinkReleased() called
requestDisallowInterceptTouchEvent(false) on every non-DOWN event whenever no
link was pressed, which undid the Editor's request on the next move. A
scrolling parent then intercepted the drag, the TextView got ACTION_CANCEL, and
the selection ended without handles or the selection toolbar.

Track whether cancelJSTouchForLinkTap() made the request and only undo our own.
@voida6

voida6 commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

Hi @hryhoriiK97, thanks for merging #1011! Could you also approve the CI workflow runs for this one? They are still waiting for approval because the PR comes from a fork.

@hryhoriiK97

Copy link
Copy Markdown
Collaborator

Thanks for tracking this down! I reproduced it on an Android emulator: on main, a long-press drag inside a ScrollView was cut off with no handles or toolbar, and with your fix it worked. I pushed a small refactor on top that keeps the same behavior but moves the parent-intercept handling into one place:

  • LinkLongPressMovementMethod now requests disallow-intercept on ACTION_DOWN over a link and releases it itself when the press ends (slop exceeded, ACTION_UP, ACTION_CANCEL), only if it made the request. The flag is private and resets on every ACTION_DOWN.
  • cancelJSTouchForLinkTap now only notifies the JS gesture system; reallowParentInterceptIfLinkReleased is gone, and the three host views just call cancelJSTouchForLinkTap on ACTION_DOWN.

I re-tested on the emulator: long-press drag selection (down and sideways), scrolling, link taps and drags starting on links, table scrolling, and the #475 case (links inside an RNGH Pressable, commonmark and github flavors).

@hryhoriiK97
hryhoriiK97 merged commit 6b7bb97 into software-mansion:main Oct 18, 2026
19 checks passed
@voida6

voida6 commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants