[General] Add minimumAnimationDuration prop to button component#4046
[General] Add minimumAnimationDuration prop to button component#4046j-piasecki wants to merge 1 commit into@jpiasecki/refactor-buttonfrom
minimumAnimationDuration prop to button component#4046Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new minimumAnimationDuration prop to GestureHandlerButton to ensure the “pressed in” visual state remains visible for at least a minimum time (mitigating native touch delays inside scrollables that can cause immediate press-out).
Changes:
- Introduces
minimumAnimationDurationto the button’s codegen spec and TS props. - Implements minimum-duration press-out deferral on Web, iOS, and Android.
- Updates the common-app underlay example to exercise the new prop.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonNativeComponent.ts | Adds minimumAnimationDuration to native component codegen props with a default. |
| packages/react-native-gesture-handler/src/components/GestureHandlerButton.web.tsx | Implements min-duration press-out deferral for web button visuals. |
| packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx | Documents the new prop on the public ButtonProps interface. |
| packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm | Wires the new prop from Fabric props into the native button view. |
| packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm | Adds iOS minimum-duration handling between press-in and press-out animations. |
| packages/react-native-gesture-handler/apple/RNGestureHandlerButton.h | Exposes minimumAnimationDuration on the native button class. |
| packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt | Adds Android prop setter + min-duration delayed press-out implementation. |
| apps/common-app/src/new_api/components/button_underlay/index.tsx | Updates example props to demonstrate minimumAnimationDuration. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export const ButtonComponent = ({ | ||
| enabled = true, | ||
| animationDuration = 100, | ||
| minimumAnimationDuration = 0, |
There was a problem hiding this comment.
minimumAnimationDuration defaults to 0 here, but the public TS docs for ButtonProps and the native codegen spec define a default of 50ms. This creates cross-platform behavior differences when the prop is omitted; consider defaulting to 50 here to match the documented/native default.
| minimumAnimationDuration = 0, | |
| minimumAnimationDuration = 50, |
| const pressOut = React.useCallback(() => { | ||
| setPressed(false); | ||
| }, []); | ||
| const elapsed = Date.now() - pressInTimestamp.current; | ||
| const remaining = | ||
| Math.min(animationDuration, minimumAnimationDuration) - elapsed; | ||
|
|
||
| if (remaining > 0) { | ||
| pressOutTimer.current = setTimeout(() => { | ||
| pressOutTimer.current = null; | ||
| setPressed(false); | ||
| }, remaining); |
There was a problem hiding this comment.
In pressOut, a new timeout is assigned to pressOutTimer.current without clearing any previously scheduled timeout. If pressOut fires multiple times (or after a quick re-press), an older timeout can still run later and flip pressed to false unexpectedly. Clear any existing pressOutTimer.current before scheduling a new one (and consider nulling it on the immediate path).
|
|
||
| var exclusive = true | ||
| var animationDuration: Int = 100 | ||
| var minimumAnimationDuration: Int = 0 |
There was a problem hiding this comment.
minimumAnimationDuration is initialized to 0, but the JS API docs and native codegen spec define a default of 50ms. To keep behavior consistent when the prop is omitted (similar to how animationDuration defaults are handled), initialize this to 50 instead of 0.
| var minimumAnimationDuration: Int = 0 | |
| var minimumAnimationDuration: Int = 50 |
| private fun animatePressOut() { | ||
| animateTo(defaultOpacity, defaultScale, defaultUnderlayOpacity) | ||
| val animationTime = SystemClock.uptimeMillis() - pressInTimestamp | ||
| val remainingAnimationTime = min(animationDuration, minimumAnimationDuration) - animationTime | ||
|
|
||
| if (remainingAnimationTime <= 0) { | ||
| animateTo(defaultOpacity, defaultScale, defaultUnderlayOpacity) | ||
| } else { | ||
| val runnable = Runnable { | ||
| pendingPressOut = null | ||
| animateTo(defaultOpacity, defaultScale, defaultUnderlayOpacity) | ||
| } | ||
| pendingPressOut = runnable | ||
| handler.postDelayed(runnable, remainingAnimationTime) | ||
| } |
There was a problem hiding this comment.
animatePressOut schedules a new Runnable and overwrites pendingPressOut without removing any previously posted runnable. If animatePressOut is invoked multiple times before the delay fires, earlier runnables can still execute after a subsequent animatePressIn, causing an unexpected press-out animation. Remove callbacks for any existing pendingPressOut before posting a new one (and avoid losing references to already-posted runnables).
| _defaultScale = 1.0; | ||
| _activeUnderlayOpacity = 0.0; | ||
| _defaultUnderlayOpacity = 0.0; | ||
| _minimumAnimationDuration = 0; |
There was a problem hiding this comment.
_minimumAnimationDuration is initialized to 0, but the JS API docs and codegen spec define a default of 50ms. Initializing this to 50 would keep default behavior consistent across platforms/architectures when the prop is omitted.
| _minimumAnimationDuration = 0; | |
| _minimumAnimationDuration = 50; |
Description
Native platforms delay touches when the button is inside a ScrollView. This may cause situations where press-out is triggered immediately after press-in, effectively running with no animation.
This PR adds
minimumAnimationDuration, which can be used to set a minimum time for the press-in animation before press-out is allowed to animate.Test plan
Updated the button underlay example