fix: prevent programmatic scrolls from cancelling active touches on iOS#57546
Open
tjzel wants to merge 2 commits into
Open
fix: prevent programmatic scrolls from cancelling active touches on iOS#57546tjzel wants to merge 2 commits into
tjzel wants to merge 2 commits into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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