@@ -2,7 +2,9 @@ import * as React from 'react';
22import type { ColorValue , NativeSyntheticEvent , ViewProps } from 'react-native' ;
33import { View } from 'react-native' ;
44
5+ import type { ButtonEvent } from '../specs/RNGestureHandlerButtonNativeComponent' ;
56import { NativeGestureRole } from '../web/interfaces' ;
7+ import { ButtonEventName } from '../web/tools/ButtonEvents' ;
68import { GestureLifecycleEvent } from '../web/tools/GestureLifecycleEvents' ;
79
810const prefersReducedMotion = ( ) : boolean =>
@@ -28,6 +30,21 @@ type ButtonProps = ViewProps & {
2830 defaultScale ?: number ;
2931 defaultUnderlayOpacity ?: number ;
3032 underlayColor ?: ColorValue ;
33+ onButtonPress ?:
34+ | ( ( event : NativeSyntheticEvent < ButtonEvent > ) => void )
35+ | undefined ;
36+ onButtonPressIn ?:
37+ | ( ( event : NativeSyntheticEvent < ButtonEvent > ) => void )
38+ | undefined ;
39+ onButtonPressOut ?:
40+ | ( ( event : NativeSyntheticEvent < ButtonEvent > ) => void )
41+ | undefined ;
42+ onButtonLongPress ?:
43+ | ( ( event : NativeSyntheticEvent < ButtonEvent > ) => void )
44+ | undefined ;
45+ onButtonInteractionFinished ?:
46+ | ( ( event : NativeSyntheticEvent < ButtonEvent > ) => void )
47+ | undefined ;
3148} ;
3249
3350export const ButtonComponent = ( {
@@ -49,6 +66,11 @@ export const ButtonComponent = ({
4966 defaultScale = 1 ,
5067 defaultUnderlayOpacity = 0 ,
5168 underlayColor,
69+ onButtonPress,
70+ onButtonPressIn,
71+ onButtonPressOut,
72+ onButtonLongPress,
73+ onButtonInteractionFinished,
5274 style,
5375 children,
5476 ...rest
@@ -119,73 +141,119 @@ export const ButtonComponent = ({
119141 } ;
120142 } , [ ] ) ;
121143
122- const pressIn = React . useCallback (
123- ( event : NativeSyntheticEvent < unknown > ) => {
124- if ( ! enabled || ! gestureEnabledRef . current ) {
125- return ;
126- }
144+ const pressIn = React . useCallback ( ( ) => {
145+ if ( ! enabled || ! gestureEnabledRef . current ) {
146+ return ;
147+ }
127148
128- event . stopPropagation ( ) ;
129- if ( pressOutTimer . current != null ) {
130- clearTimeout ( pressOutTimer . current ) ;
131- pressOutTimer . current = null ;
132- }
133- pressInTimestamp . current = performance . now ( ) ;
134- setCurrentDuration ( tapAnimationInDuration ) ;
135- setPressed ( true ) ;
136- } ,
137- [ enabled , tapAnimationInDuration ]
138- ) ;
149+ if ( pressOutTimer . current != null ) {
150+ clearTimeout ( pressOutTimer . current ) ;
151+ pressOutTimer . current = null ;
152+ }
153+ pressInTimestamp . current = performance . now ( ) ;
154+ setCurrentDuration ( tapAnimationInDuration ) ;
155+ setPressed ( true ) ;
156+ } , [ enabled , tapAnimationInDuration ] ) ;
139157
140- const pressOut = React . useCallback (
141- ( event : NativeSyntheticEvent < unknown > ) => {
142- // Only release if a press-in was actually recorded — guards against
143- // stray pointer events and lets us complete the release cycle even if
144- // `enabled` flipped to false between press-in and press-out.
145- if ( pressInTimestamp . current === 0 || ! gestureEnabledRef . current ) {
146- return ;
147- }
158+ const pressOut = React . useCallback ( ( ) => {
159+ // Only release if a press-in was actually recorded — guards against
160+ // stray pointer events and lets us complete the release cycle even if
161+ // `enabled` flipped to false between press-in and press-out.
162+ if ( pressInTimestamp . current === 0 || ! gestureEnabledRef . current ) {
163+ return ;
164+ }
148165
149- event . stopPropagation ( ) ;
150- if ( pressOutTimer . current != null ) {
151- clearTimeout ( pressOutTimer . current ) ;
152- pressOutTimer . current = null ;
153- }
154- const elapsed = performance . now ( ) - pressInTimestamp . current ;
155- pressInTimestamp . current = 0 ;
166+ if ( pressOutTimer . current != null ) {
167+ clearTimeout ( pressOutTimer . current ) ;
168+ pressOutTimer . current = null ;
169+ }
170+ const elapsed = performance . now ( ) - pressInTimestamp . current ;
171+ pressInTimestamp . current = 0 ;
156172
157- if ( longPressDuration >= 0 && elapsed >= longPressDuration ) {
158- // Long-press release — use the configured long-press out duration.
159- setCurrentDuration ( longPressAnimationOutDuration ) ;
160- setPressed ( false ) ;
161- } else if ( elapsed >= tapAnimationInDuration ) {
162- // Press-in animation fully finished - release with the configured out duration.
163- setCurrentDuration ( tapAnimationOutDuration ) ;
164- setPressed ( false ) ;
165- // elapsed * 2 to ensure there is at least half of the tapAnimationOutDuration left for the animation to play
166- } else if ( elapsed * 2 >= tapAnimationOutDuration ) {
167- setCurrentDuration ( elapsed ) ;
168- setPressed ( false ) ;
169- } else {
170- // Let the in-progress CSS press-in transition continue; schedule press-out after remaining time.
171- const remaining = tapAnimationInDuration - elapsed ;
172- pressOutTimer . current = setTimeout (
173- ( ) => {
174- pressOutTimer . current = null ;
175- setCurrentDuration ( tapAnimationOutDuration ) ;
176- setPressed ( false ) ;
177- } ,
178- prefersReducedMotion ( ) ? 0 : remaining
179- ) ;
180- }
181- } ,
182- [
183- longPressDuration ,
184- longPressAnimationOutDuration ,
185- tapAnimationInDuration ,
186- tapAnimationOutDuration ,
187- ]
188- ) ;
173+ if ( longPressDuration >= 0 && elapsed >= longPressDuration ) {
174+ // Long-press release — use the configured long-press out duration.
175+ setCurrentDuration ( longPressAnimationOutDuration ) ;
176+ setPressed ( false ) ;
177+ } else if ( elapsed >= tapAnimationInDuration ) {
178+ // Press-in animation fully finished - release with the configured out duration.
179+ setCurrentDuration ( tapAnimationOutDuration ) ;
180+ setPressed ( false ) ;
181+ // elapsed * 2 to ensure there is at least half of the tapAnimationOutDuration left for the animation to play
182+ } else if ( elapsed * 2 >= tapAnimationOutDuration ) {
183+ setCurrentDuration ( elapsed ) ;
184+ setPressed ( false ) ;
185+ } else {
186+ // Let the in-progress CSS press-in transition continue; schedule press-out after remaining time.
187+ const remaining = tapAnimationInDuration - elapsed ;
188+ pressOutTimer . current = setTimeout (
189+ ( ) => {
190+ pressOutTimer . current = null ;
191+ setCurrentDuration ( tapAnimationOutDuration ) ;
192+ setPressed ( false ) ;
193+ } ,
194+ prefersReducedMotion ( ) ? 0 : remaining
195+ ) ;
196+ }
197+ } , [
198+ longPressDuration ,
199+ longPressAnimationOutDuration ,
200+ tapAnimationInDuration ,
201+ tapAnimationOutDuration ,
202+ ] ) ;
203+
204+ React . useEffect ( ( ) => {
205+ const node = viewRef . current ;
206+ const wrapEvent = ( event : Event ) : NativeSyntheticEvent < ButtonEvent > =>
207+ ( {
208+ nativeEvent : ( event as CustomEvent < ButtonEvent > ) . detail ,
209+ } ) as NativeSyntheticEvent < ButtonEvent > ;
210+
211+ const handlePress = ( event : Event ) => {
212+ onButtonPress ?.( wrapEvent ( event ) ) ;
213+ } ;
214+ const handlePressIn = ( event : Event ) => {
215+ pressIn ( ) ;
216+ onButtonPressIn ?.( wrapEvent ( event ) ) ;
217+ } ;
218+ const handlePressOut = ( event : Event ) => {
219+ pressOut ( ) ;
220+ onButtonPressOut ?.( wrapEvent ( event ) ) ;
221+ } ;
222+ const handleLongPress = ( event : Event ) => {
223+ onButtonLongPress ?.( wrapEvent ( event ) ) ;
224+ } ;
225+ const handleInteractionFinished = ( event : Event ) => {
226+ onButtonInteractionFinished ?.( wrapEvent ( event ) ) ;
227+ } ;
228+
229+ node ?. addEventListener ( ButtonEventName . Press , handlePress ) ;
230+ node ?. addEventListener ( ButtonEventName . PressIn , handlePressIn ) ;
231+ node ?. addEventListener ( ButtonEventName . PressOut , handlePressOut ) ;
232+ node ?. addEventListener ( ButtonEventName . LongPress , handleLongPress ) ;
233+ node ?. addEventListener (
234+ ButtonEventName . InteractionFinished ,
235+ handleInteractionFinished
236+ ) ;
237+
238+ return ( ) => {
239+ node ?. removeEventListener ( ButtonEventName . Press , handlePress ) ;
240+ node ?. removeEventListener ( ButtonEventName . PressIn , handlePressIn ) ;
241+ node ?. removeEventListener ( ButtonEventName . PressOut , handlePressOut ) ;
242+ node ?. removeEventListener ( ButtonEventName . LongPress , handleLongPress ) ;
243+ node ?. removeEventListener (
244+ ButtonEventName . InteractionFinished ,
245+ handleInteractionFinished
246+ ) ;
247+ } ;
248+ } , [
249+ onButtonInteractionFinished ,
250+ onButtonLongPress ,
251+ onButtonPress ,
252+ onButtonPressIn ,
253+ onButtonPressOut ,
254+ pressIn ,
255+ pressOut ,
256+ ] ) ;
189257
190258 const handlePointerEnter = React . useCallback (
191259 ( event : NativeSyntheticEvent < { pointerType ?: string } > ) => {
@@ -203,7 +271,6 @@ export const ButtonComponent = ({
203271
204272 const handlePointerLeave = React . useCallback (
205273 ( event : NativeSyntheticEvent < { pointerType ?: string } > ) => {
206- pressOut ( event ) ;
207274 if ( event . nativeEvent . pointerType === 'touch' ) {
208275 return ;
209276 }
@@ -212,7 +279,7 @@ export const ButtonComponent = ({
212279 }
213280 setHovered ( false ) ;
214281 } ,
215- [ pressOut , pressed , hoverAnimationOutDuration ]
282+ [ pressed , hoverAnimationOutDuration ]
216283 ) ;
217284
218285 // Mask hover at render rather than clearing the state. Avoids a state
@@ -268,9 +335,6 @@ export const ButtonComponent = ({
268335 } ,
269336 ] }
270337 onPointerEnter = { handlePointerEnter }
271- onPointerDown = { pressIn }
272- onPointerUp = { pressOut }
273- onPointerCancel = { pressOut }
274338 onPointerLeave = { handlePointerLeave } >
275339 { hasUnderlay && (
276340 < View
0 commit comments