Repository navigation
fix(button): prevent false ripples during scroll deceleration (#4911) - #5133
Priyanshu0007 wants to merge 3 commits into
Conversation
|
Found potential problems with the pull request:
|
MikitasK
left a comment
There was a problem hiding this comment.
good job 👍
the fix looks good: I reproduced the bug & fix, so the direction is right 💯
overall:
- 2 things looks blocking to me:
Build packageCI step & test coverage - 1 coordination point: #5097 rewrites most of
Button.tsx, so worth agreeing on sequencing before you spend more time here - the rest is discussion, not blocking
| /** | ||
| * Delay in ms, from the start of the touch, before the press is engaged and ripple is triggered. | ||
| */ | ||
| unstable_pressDelay?: number; |
There was a problem hiding this comment.
could we check how this lands against #5097 ? that PR is actively rewriting this file & already covers all 3 places this PR edits, so it'll definitely conflict.
since 6.x is moving Button to Expressive, what do you think about rebasing on it?
There was a problem hiding this comment.
[blocker] another thing to flag: Build package CI step will fail for this PR (I reproduced it locally)
API leak guard in scripts/typescript-output-lint.ts sees unstable_pressDelay in generated Button.d.ts & flags it as unexpected RN prop.
Now that Button defines it, could we just remove it from unusedViewProps at scripts/typescript-output-lint.ts ?
There was a problem hiding this comment.
| expect(screen.getByTestId('active-button')).toBeEnabled(); | ||
| }); | ||
|
|
||
| it('renders button with unstable_pressDelay and triggers onPress', async () => { |
There was a problem hiding this comment.
[blocker] could we strengthen these?
I tried running this test after reverting changes in Button.tsx. Result - test passed, so seems like nothing here actually guards the delay.
userEvent.press resolves regardless of it. also RNL 14 removed UNSAFE_getByType that's why prop assertion isn't available either.
so wdyt about driving RN's responder state machine with fake timers asserting pressIn is suppressed on RESPONDER_TERMINATED?
| testID, | ||
| }: IconProps) => React.ReactNode; | ||
| rippleEffectEnabled?: boolean; | ||
| ripplePressDelay?: number; |
There was a problem hiding this comment.
could we keep this PR to per-component unstable_pressDelay prop & leave global ripplePressDelay for separate discussion?
#4911 is about a button inside scrolling list, but this setting changes every TouchableRipple-based component app-wide the moment someone sets it on PaperProvider.
it's also not fully global: Card uses its own Pressable with unstable_pressDelay={0} so pressable Card in a list would still misbehave.
for what it's worth, #3806 (same problem) was solved at the call site rather than with library-wide default. the wiring itself looks fine & consistent with rippleEffectEnabled - mainly wanted to keep this PR's surface area small
| onPressIn={hasPassedTouchHandler ? handlePressIn : undefined} | ||
| onPressOut={hasPassedTouchHandler ? handlePressOut : undefined} | ||
| delayLongPress={delayLongPress} | ||
| unstable_pressDelay={unstable_pressDelay} |
There was a problem hiding this comment.
what do you think about fast-tap case?
with a delay set, tap shorter than it still fires onPress but with no visible feedback at all - close to the mirror image of original complaint.
could we document the trade-off on the prop? and/or suggest a smaller value maybe?
There was a problem hiding this comment.
Done,
I've added a note explaining this trade-off (that fast taps shorter than the delay will still trigger onPress but might not show the ripple animation) and recommended using small values (e.g. 50–100ms) to strike the right balance between scroll cancellation and touch feedback.
| */ | ||
| delayLongPress?: number; | ||
| /** | ||
| * Delay in ms, from the start of the touch, before the press is engaged and ripple is triggered. |
There was a problem hiding this comment.
[nit] I tested it out & seems like press still fires - only feedback is delayed
so maybe we could reword it to smth like "...before the press feedback (ripple) is shown"?
1981b0c to
459e4a5
Compare
|
Thanks a lot @MikitasK for the review! I've addressed all your points dropped the global setting to keep the scope small, updated the prop docs with the fast tap note, added proper responder/timer tests, and rebased onto Could you please take another look whenever you get a chance? |
MikitasK
left a comment
There was a problem hiding this comment.
thanks for the quick turnaround 🙏
earlier points are all addressed & code itself looks good to me 👍
there are just several blockers remaining:
-
description is stale: it still advertises
ripplePressDelayonPaperProvider/SettingsContext&TouchableRipplechanges, which are gone.
could you rewrite it aroundButtonprop only? -
could you attach before/after media per platform (iOS, Android, web)?
-
a few inline comments:
|
|
||
| const button = screen.getByTestId('delay-button'); | ||
|
|
||
| await fireEvent(button, 'responderGrant', createResponderEvent()); |
There was a problem hiding this comment.
could we add one more test for the most common case - quick tap that actually completes?
right now 2 tests cover:
- touch ends before the delay via
responderTerminate-> no ripple, no press - touch is held past the delay -> ripple shows, press fires
what's missing is in-between: user taps for less than the delay & lifts finger normally (responderRelease).
That's the everyday fast tap, and it's the case new docblock promises works: onPress fires, only ripple is skipped.
so could we add: grant -> advanceTimersByTime(60) -> responderRelease, then expect onPress to have been called & onPressIn not to have been called
| /** | ||
| * Delay in ms, from the start of the touch, before the press feedback (ripple) is shown. | ||
| * Quick taps shorter than this delay will still fire `onPress` but might not show the ripple effect. | ||
| * Recommended values for buttons in scrollable lists are between 50ms and 100ms. |
There was a problem hiding this comment.
[nit] could we tag this native-only, like background is marked "(Android)" above?
react-native-web doesn't implement unstable_pressDelay, so does nothing on web
|
Thanks for the feedback @MikitasK ! I've addressed all the remaining points and pushed the updates:
Could you please take another look when you have a moment? |
Summary
This PR exposes the
unstable_pressDelayprop onButton(forwarded to the underlyingTouchableRipple/Pressable).Motivation & Problem
When scrolling or flicking through a scrollable list (such as
ScrollVieworFlatList) containing buttons, touching the screen to stop deceleration can inadvertently trigger the button's ripple effect before the gesture responder transfers to the scroll container (responderTerminate).Solution
Adding
unstable_pressDelayallows configuring a brief delay (recommended 50-100ms) before the touch feedback is activated:responderTerminate) within this delay window, no ripple effect is displayed.onPressas expected.(Native-only)asreact-native-webdoes not implementunstable_pressDelay.Test Plan
Button.test.tsxandTouchableRipple.test.tsxverifying:onPressInandonPress.onPressInandonPress.onPressInduring touch and successfully triggeronPresson release.Related issue
Fixes #4911
Evidence media
iOS Media
button-scroll-before-ios.mp4
button-scroll-after-ios.mp4
Android Media
button-scroll-before-android.mp4
button-scroll-after-android.mp4
Web Media
button-scroll-before-web.mov
button-scroll-after-web.mov