Skip to content

fix(button): prevent false ripples during scroll deceleration (#4911) - #5133

Open
Priyanshu0007 wants to merge 1 commit into
callstack:mainfrom
Priyanshu0007:fix/button-scroll-deceleration-ripple
Open

Priyanshu0007 wants to merge 1 commit into
callstack:mainfrom
Priyanshu0007:fix/button-scroll-deceleration-ripple

Conversation

@Priyanshu0007

Copy link
Copy Markdown

Motivation

When tapping or touching an active Button or TouchableRipple inside a decelerating scroll view (or during quick list fling/scroll cancellation touches), the native button immediately triggers an ink ripple animation. However, when the parent ScrollView detects scrolling motion, it steals the touch gesture (cancelling the press and onPress). The button is never activated, leaving the user with false visual feedback.

This PR addresses the issue by:

  1. Exposing unstable_pressDelay?: number on Button and TouchableRipple props to forward to React Native's underlying Pressable.
  2. Providing a global ripplePressDelay?: number configuration via SettingsContext (PaperProvider), allowing apps to configure a global press delay (e.g. 50-100ms) across all paper ripple surfaces without having to pass the prop to every component manually.

Related issue

Fixes #4911

Test plan

  1. Automated Tests:

    • Added unit test in Button.test.tsx verifying unstable_pressDelay triggers onPress correctly.
    • Added unit tests in TouchableRipple.test.tsx verifying both unstable_pressDelay prop and ripplePressDelay from SettingsContext.
    • Verified all existing unit tests, linter, and type checks pass cleanly (yarn test, yarn lint, yarn typecheck).
  2. Manual Verification (Android):

    • Place buttons in a scrollable list inside <ScrollView>.
    • Configure <PaperProvider settings={{ ripplePressDelay: 80 }}> or add unstable_pressDelay={80} to <Button>.
    • Fling the scroll list and tap a button mid-deceleration: scroll stops cleanly with no false ripple.
    • Tap the button while stationary: ripple and onPress trigger as expected.

@github-actions

Copy link
Copy Markdown

Found potential problems with the pull request:

  • The description is too long. Please keep it under 1000 characters.
  • Screenshot or video evidence is missing. Make sure to include one if it affects the UI.

@MikitasK MikitasK left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good job 👍
the fix looks good: I reproduced the bug & fix, so the direction is right 💯

overall:

  • 2 things looks blocking to me: Build package CI 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@MikitasK MikitasK Oct 30, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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 ?

expect(screen.getByTestId('active-button')).toBeEnabled();
});

it('renders button with unstable_pressDelay and triggers onPress', async () => {

@MikitasK MikitasK Oct 30, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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?

Comment thread src/core/settings.tsx
testID,
}: IconProps) => React.ReactNode;
rippleEffectEnabled?: boolean;
ripplePressDelay?: number;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

*/
delayLongPress?: number;
/**
* Delay in ms, from the start of the touch, before the press is engaged and ripple is triggered.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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"?

This branch has not been deployed

No deployments
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.

Buttons show false feedback when tapped during scroll deceleration

2 participants