fix: prevent programmatic scrolls from cancelling active touches on iOS - #57546
fix: prevent programmatic scrolls from cancelling active touches on iOS#57546tjzel wants to merge 2 commits into
Conversation
|
Great Job @tjzel One Question:
A multiline It dispatches under the same So an ancestor Android already covers this by a different route. Its text input dispatches the shared and the field is declared in Android's TextInput A constant is enough here, since a text input scrolling its own content is never a reason for an ancestor to claim the touch: payload.setProperty(runtime, "responderIgnoreScroll", true);We're running your change as a patch on 0.85.3 and it fixed the WDYT? |
Summary:
On iOS, a programmatic non-animated scroll —
scrollTo/scrollToOffset({ animated: false }), or any library driving the offset frame-by-frame — cancels every active touch in enclosing scroll views. Two mechanisms combine into this:scrollToOffset:animated:calls_forceDispatchNextScrollEventand, for non-animated scrolls,_handleFinishedScrolling— so every call emitsonScroll(twice) plusonMomentumScrollEnd, bypassingscrollEventThrottleentirely. A per-frame driver produces a continuous stream of unthrottledtopScrollevents (~60/s measured withscrollEventThrottle={2000}).topScrollevent withoutresponderIgnoreScroll: truestarts a responder negotiation, andScrollView'sonScrollShouldSetResponderanswerstruewhenever a finger is down inside it. Each event therefore steals the responder from a pressedPressable/Touchableand the press is cancelled —onPressInfires,onPressnever does.On Android scroll events carry
responderIgnoreScroll: true.I added
responderIgnoreScrollto the C++ScrollEventpayload and set it to!_isUserTriggeredScrollingin_scrollViewMetrics. Programmatic scrolls no longer transfer the responder, while user-initiated scrolls (drag, deceleration, scroll-to-top) keep today's behavior.Changelog:
[IOS] [FIXED] - Programmatic (non-user-initiated) scrolls no longer cancel active touches in enclosing scroll views
Test Plan:
Reproducible code — an endless marquee
FlatListnested in aScrollView, driven byrequestAnimationFrame+scrollToOffset({ animated: false }), with a siblingTouchableOpacityand a counter proving the touches reach JS:App.tsx
Before this change, only the first tap works (it starts the marquee); every following tap increments the touch counter but never toggles the button — the press is cancelled by the responder transfer. After this change, every tap toggles the marquee.
Recordings of the repro above (every touch is marked with a blue ring and counted on screen):
Before:
bugged_trim.mp4
After:
fixed_trim.mp4