Skip to content

fix(android): release scroll gestures the input cannot consume - #731

Open
lukasz-patalan wants to merge 1 commit into
software-mansion:mainfrom
lukasz-patalan:fix/android-scroll-gesture-handoff
Open

fix(android): release scroll gestures the input cannot consume#731
lukasz-patalan wants to merge 1 commit into
software-mansion:mainfrom
lukasz-patalan:fix/android-scroll-gesture-handoff

Conversation

@lukasz-patalan

@lukasz-patalan lukasz-patalan commented Jul 29, 2026

Copy link
Copy Markdown

Summary

The problem: on Android, an EnrichedTextInput placed inside a scrollable screen always claims vertical drags that start inside it, even when it has no remaining scroll range. As a result, the surrounding ScrollView never receives those gestures, making the screen feel frozen whenever the drag starts over the editor.

What this PR does: the editor keeps handling drags while it can still scroll, and releases them to its parent as soon as it reaches the content boundary.

Why it happens

onTouchEvent already contains the correct logic to release gestures when the editor cannot scroll in any direction:

if (!canScrollVertically(-1) && !canScrollVertically(1) &&
    !canScrollHorizontally(-1) && !canScrollHorizontally(1)
) {
  // We cannot scroll, let parent views take care of these touches.
  this.parent.requestDisallowInterceptTouchEvent(false)
}

This check comes directly from React Native's ReactEditText (linked in the comment above onTouchEvent). There it works because canScrollVertically() and canScrollHorizontally() come from TextView, which report whether there is actually more content to scroll.

This library overrides both methods to return only the scrollEnabled flag. With the default scrollEnabled={true}, the condition above is therefore never satisfied, so the parent never regains the gesture. scrollEnabled={false} already behaves correctly today because both methods return false.

Implementation

  • canScrollVertically() and canScrollHorizontally() now return scrollEnabled && super.canScroll..., restoring the original content-aware behaviour.
  • onTouchEvent records the touch origin and waits until the pointer moves beyond ViewConfiguration.getScaledTouchSlop() before deciding who owns the gesture. This keeps taps, long presses and text selection unchanged.
  • If a vertical drag reaches an edge the editor cannot scroll past, it stops disallowing parent interception and returns false, allowing the parent ScrollView to continue the same gesture.
  • detectScrollMovement is reset on ACTION_UP and ACTION_CANCEL, preventing the decision from leaking into subsequent gestures.

Behaviour

Case Before After
scrollEnabled={true}, content fits nothing scrolls parent scrolls
scrollEnabled={true}, content overflows, drag mid-content editor scrolls editor scrolls (unchanged)
scrollEnabled={true}, content overflows, drag at top/bottom edge nothing scrolls parent scrolls
scrollEnabled={false} parent scrolls parent scrolls (unchanged)
tap / long press / text selection / horizontal drag unchanged unchanged

No public API changes are introduced. The only behavioural change is that, with the default scrollEnabled={true}, vertical drags the editor cannot consume are now passed to the parent instead of being swallowed.

Use case

We recently moved our WYSIWYG editor from a dedicated full-screen view into regular forms, where it sits inside a parent ScrollView as one field among many. Two things pushed us towards this patch, both coming from the input holding on to touches it cannot use:

  • Dragging over a focused editor moved the caret or started a text selection instead of scrolling the form. Releasing the parent's interception is not enough on its own here, because the editor still passes the gesture to super.onTouchEvent and consumes it as a caret move.
  • Dismissing the keyboard by tapping outside the editor, which we handle with react-native-click-outside, needs those touches to reach the ancestor views, otherwise the tap never gets a chance to blur the input.

Test Plan

Reproduce in the example app's Test screen (the editor is already inside a ScrollView).

Before this change, with content short enough not to overflow the editor, dragging vertically over the editor does not scroll the parent.

After this change:

  1. Short content: the parent ScrollView scrolls.
  2. Long content (Max size mode), drag mid-content: the editor scrolls exactly as before.
  3. Long content, editor at its top or bottom edge: dragging further transfers the gesture to the parent.
  4. scrollEnabled={false}, taps, long presses and text selection remain unchanged.

Verified locally on an Android emulator (Pixel9, API 36, arm64).

Both scrolling_after_typing and scrolling_set_value pass.

Compatibility

OS Implemented
iOS
Android
Web

We could not reproduce this issue on iOS. UITextView is backed by UIScrollView, whose gesture handling already appears to behave correctly in this scenario.

Checklist

  • E2E tests are passing
  • Required E2E tests have been added (if applicable)

Copilot AI review requested due to automatic review settings July 29, 2026 08:40

Copilot AI 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.

Pull request overview

This PR updates Android touch/scroll handling in EnrichedTextInputView so vertical drags that the editor can’t consume (because it’s not scrollable or is at a content boundary) can be handled by an enclosing scroll container (e.g., ScrollView), instead of being swallowed by the editor.

Changes:

  • Restore content-aware scrollability by making canScrollVertically / canScrollHorizontally depend on both scrollEnabled and super’s scroll range.
  • Track the initial touch position and use ViewConfiguration touch slop to avoid changing tap/selection behavior while still enabling gesture handoff for real drags.
  • Add logic intended to release parent interception when a vertical drag can’t be consumed.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 334 to 338
) {
// We cannot scroll, let parent views take care of these touches.
this.parent.requestDisallowInterceptTouchEvent(false)
}
detectScrollMovement = false
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