From dad2eaa2b8bdaede8023a512a454a852b551412a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Mon, 3 Nov 2025 11:19:56 +0100 Subject: [PATCH 01/21] new file --- .../src/v3/createNativeWrapper.tsx | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx diff --git a/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx b/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx new file mode 100644 index 0000000000..d8c572a597 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx @@ -0,0 +1,87 @@ +import * as React from 'react'; +import { useImperativeHandle, useRef } from 'react'; + +import { + NativeViewGestureHandler, + NativeViewGestureHandlerProps, + nativeViewProps, +} from '../handlers/NativeViewGestureHandler'; + +/* + * This array should consist of: + * - All keys in propTypes from NativeGestureHandler + * (and all keys in GestureHandlerPropTypes) + * - 'onGestureHandlerEvent' + * - 'onGestureHandlerStateChange' + */ +const NATIVE_WRAPPER_PROPS_FILTER = [ + ...nativeViewProps, + 'onGestureHandlerEvent', + 'onGestureHandlerStateChange', +] as const; + +export default function createNativeWrapper

( + Component: React.ComponentType

, + config: Readonly = {} +) { + const ComponentWrapper = (props: P & NativeViewGestureHandlerProps) => { + // Filter out props that should be passed to gesture handler wrapper + const { gestureHandlerProps, childProps } = Object.keys(props).reduce( + (res, key) => { + // TS being overly protective with it's types, see https://github.com/microsoft/TypeScript/issues/26255#issuecomment-458013731 for more info + const allowedKeys: readonly string[] = NATIVE_WRAPPER_PROPS_FILTER; + if (allowedKeys.includes(key)) { + // @ts-ignore FIXME(TS) + res.gestureHandlerProps[key] = props[key]; + } else { + // @ts-ignore FIXME(TS) + res.childProps[key] = props[key]; + } + return res; + }, + { + gestureHandlerProps: { ...config }, // Watch out not to modify config + childProps: { + enabled: props.enabled, + hitSlop: props.hitSlop, + testID: props.testID, + } as P, + } + ); + const _ref = useRef>(null); + const _gestureHandlerRef = useRef>(null); + useImperativeHandle( + props.ref, + // @ts-ignore TODO(TS) decide how nulls work in this context + () => { + const node = _gestureHandlerRef.current; + // Add handlerTag for relations config + if (_ref.current && node) { + // @ts-ignore FIXME(TS) think about createHandler return type + _ref.current.handlerTag = node.handlerTag; + return _ref.current; + } + return null; + }, + [_ref, _gestureHandlerRef] + ); + return ( + + + + ); + }; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + ComponentWrapper.displayName = + Component?.displayName || + // @ts-ignore if render doesn't exist it will return undefined and go further + Component?.render?.name || + (typeof Component === 'string' && Component) || + 'ComponentWrapper'; + + return ComponentWrapper; +} From 6dbc10d51813add4fa748500166643f055cb7834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Mon, 3 Nov 2025 14:21:03 +0100 Subject: [PATCH 02/21] use native --- .../src/v3/createNativeWrapper.tsx | 65 +++++-------------- .../src/v3/hooks/utils/propsWhiteList.ts | 28 +++++--- .../src/v3/types/GestureTypes.ts | 2 +- .../src/v3/types/NativeWrapperType.ts | 7 ++ 4 files changed, 44 insertions(+), 58 deletions(-) create mode 100644 packages/react-native-gesture-handler/src/v3/types/NativeWrapperType.ts diff --git a/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx b/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx index d8c572a597..19dd2b4199 100644 --- a/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx +++ b/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx @@ -1,36 +1,22 @@ import * as React from 'react'; -import { useImperativeHandle, useRef } from 'react'; -import { - NativeViewGestureHandler, - NativeViewGestureHandlerProps, - nativeViewProps, -} from '../handlers/NativeViewGestureHandler'; - -/* - * This array should consist of: - * - All keys in propTypes from NativeGestureHandler - * (and all keys in GestureHandlerPropTypes) - * - 'onGestureHandlerEvent' - * - 'onGestureHandlerStateChange' - */ -const NATIVE_WRAPPER_PROPS_FILTER = [ - ...nativeViewProps, - 'onGestureHandlerEvent', - 'onGestureHandlerStateChange', -] as const; +import { NativeWrapperProps } from './hooks/utils'; +import { useNative } from './hooks/gestures'; +import { NativeDetector } from './NativeDetector/NativeDetector'; +import type { NativeWrapperProperties } from './types/NativeWrapperType'; export default function createNativeWrapper

( Component: React.ComponentType

, - config: Readonly = {} + config: Readonly = {} ) { - const ComponentWrapper = (props: P & NativeViewGestureHandlerProps) => { + const ComponentWrapper = ( + props: P & NativeWrapperProperties & { ref: React.RefObject } + ) => { // Filter out props that should be passed to gesture handler wrapper const { gestureHandlerProps, childProps } = Object.keys(props).reduce( (res, key) => { - // TS being overly protective with it's types, see https://github.com/microsoft/TypeScript/issues/26255#issuecomment-458013731 for more info - const allowedKeys: readonly string[] = NATIVE_WRAPPER_PROPS_FILTER; - if (allowedKeys.includes(key)) { + // @ts-ignore TS being overly protective with it's types, see https://github.com/microsoft/TypeScript/issues/26255#issuecomment-458013731 for more info + if (NativeWrapperProps.has(key)) { // @ts-ignore FIXME(TS) res.gestureHandlerProps[key] = props[key]; } else { @@ -44,34 +30,17 @@ export default function createNativeWrapper

( childProps: { enabled: props.enabled, hitSlop: props.hitSlop, - testID: props.testID, + // testID: props.testID, } as P, } ); - const _ref = useRef>(null); - const _gestureHandlerRef = useRef>(null); - useImperativeHandle( - props.ref, - // @ts-ignore TODO(TS) decide how nulls work in this context - () => { - const node = _gestureHandlerRef.current; - // Add handlerTag for relations config - if (_ref.current && node) { - // @ts-ignore FIXME(TS) think about createHandler return type - _ref.current.handlerTag = node.handlerTag; - return _ref.current; - } - return null; - }, - [_ref, _gestureHandlerRef] - ); + + const native = useNative(gestureHandlerProps); + return ( - - - + + + ); }; diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts index ecc6fc9efc..e6c137c6d6 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts @@ -1,11 +1,13 @@ import { BaseGestureConfig, CommonGestureConfig, + ExternalRelations, GestureCallbacks, HandlersPropsWhiteList, InternalConfigProps, SingleGestureName, } from '../../types'; +import { NativeWrapperProperties } from '../../types/NativeWrapperType'; import { FlingNativeProperties } from '../gestures/fling/FlingProperties'; import { HoverNativeProperties } from '../gestures/hover/HoverProperties'; import { LongPressNativeProperties } from '../gestures/longPress/LongPressProperties'; @@ -13,10 +15,7 @@ import { NativeHandlerNativeProperties } from '../gestures/native/NativeProperti import { PanNativeProperties } from '../gestures/pan/PanProperties'; import { TapNativeProperties } from '../gestures/tap/TapProperties'; -export const allowedNativeProps = new Set< - keyof CommonGestureConfig | keyof InternalConfigProps ->([ - // CommonGestureConfig +const CommonConfig = new Set([ 'enabled', 'shouldCancelWhenOutside', 'hitSlop', @@ -25,6 +24,14 @@ export const allowedNativeProps = new Set< 'mouseButton', 'enableContextMenu', 'touchAction', +]); + +const ExternalRelationsConfig = new Set([]); + +export const allowedNativeProps = new Set< + keyof CommonGestureConfig | keyof InternalConfigProps +>([ + ...CommonConfig, // InternalConfigProps 'dispatchesReanimatedEvents', @@ -48,16 +55,12 @@ export const HandlerCallbacks = new Set< export const PropsToFilter = new Set>([ ...HandlerCallbacks, + ...ExternalRelationsConfig, // Config props 'changeEventCalculator', 'disableReanimated', 'shouldUseReanimatedDetector', - - // Relations - 'simultaneousWithExternalGesture', - 'requireExternalGestureToFail', - 'blocksExternalGesture', ]); export const PropsWhiteLists = new Map< @@ -73,3 +76,10 @@ export const PropsWhiteLists = new Map< ]); export const EMPTY_WHITE_LIST = new Set(); + +export const NativeWrapperProps = new Set([ + ...CommonConfig, + ...HandlerCallbacks, + ...NativeHandlerNativeProperties, + ...ExternalRelationsConfig, +]); diff --git a/packages/react-native-gesture-handler/src/v3/types/GestureTypes.ts b/packages/react-native-gesture-handler/src/v3/types/GestureTypes.ts index a2a8df21b8..00d4259419 100644 --- a/packages/react-native-gesture-handler/src/v3/types/GestureTypes.ts +++ b/packages/react-native-gesture-handler/src/v3/types/GestureTypes.ts @@ -9,7 +9,7 @@ import { import { FilterNeverProperties } from './UtilityTypes'; // Unfortunately, this type cannot be moved into ConfigTypes.ts because of circular dependency -type ExternalRelations = { +export type ExternalRelations = { simultaneousWithExternalGesture?: Gesture | Gesture[]; requireExternalGestureToFail?: Gesture | Gesture[]; blocksExternalGesture?: Gesture | Gesture[]; diff --git a/packages/react-native-gesture-handler/src/v3/types/NativeWrapperType.ts b/packages/react-native-gesture-handler/src/v3/types/NativeWrapperType.ts new file mode 100644 index 0000000000..35684b885c --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/types/NativeWrapperType.ts @@ -0,0 +1,7 @@ +import { CommonGestureConfig, ExternalRelations, GestureCallbacks } from '.'; +import { NativeGestureNativeProperties } from '../hooks/gestures/native/NativeProperties'; + +export type NativeWrapperProperties = CommonGestureConfig & + GestureCallbacks & + NativeGestureNativeProperties & + ExternalRelations; From 4f1dcaa058907ab983eeb3ac00d294e0debdacab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Mon, 3 Nov 2025 16:21:26 +0100 Subject: [PATCH 03/21] Buttons --- .../src/components/GestureButtons.tsx | 344 +++++++----------- .../src/components/GestureButtonsProps.ts | 2 +- .../src/v3/createNativeWrapper.tsx | 2 +- .../src/v3/hooks/gestures/native/useNative.ts | 2 +- .../src/v3/hooks/utils/propsWhiteList.ts | 1 + .../src/v3/types/NativeWrapperType.ts | 3 +- 6 files changed, 147 insertions(+), 207 deletions(-) diff --git a/packages/react-native-gesture-handler/src/components/GestureButtons.tsx b/packages/react-native-gesture-handler/src/components/GestureButtons.tsx index 04409f7f2c..d2f019ecd5 100644 --- a/packages/react-native-gesture-handler/src/components/GestureButtons.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureButtons.tsx @@ -1,152 +1,116 @@ -import * as React from 'react'; -import { Animated, Platform, StyleSheet } from 'react-native'; +import React from 'react'; +import { Platform, StyleSheet } from 'react-native'; -import createNativeWrapper from '../handlers/createNativeWrapper'; +import createNativeWrapper from '../v3/createNativeWrapper'; import GestureHandlerButton from './GestureHandlerButton'; -import { State } from '../State'; - -import { - GestureEvent, - HandlerStateChangeEvent, -} from '../handlers/gestureHandlerCommon'; -import type { NativeViewGestureHandlerPayload } from '../handlers/GestureHandlerEventPayload'; import type { - BaseButtonWithRefProps, BaseButtonProps, - RectButtonWithRefProps, - RectButtonProps, - BorderlessButtonWithRefProps, BorderlessButtonProps, + RectButtonProps, } from './GestureButtonsProps'; +import Animated, { + useAnimatedStyle, + useSharedValue, +} from 'react-native-reanimated'; +import type { GestureStateChangeEvent } from '../v3/types'; +import type { NativeViewHandlerData } from '../v3/hooks/gestures/native/useNative'; + +type CallbackEventType = GestureStateChangeEvent; export const RawButton = createNativeWrapper(GestureHandlerButton, { shouldCancelWhenOutside: false, shouldActivateOnStart: false, }); -class InnerBaseButton extends React.Component { - static defaultProps = { - delayLongPress: 600, +export const BaseButton = (props: BaseButtonProps) => { + let lastActive: boolean; + let longPressDetected: boolean; + let longPressTimeout: ReturnType | undefined; + + const delayLongPress = props.delayLongPress ?? 600; + + const { + onLongPress, + onPress, + onActiveStateChange, + rippleColor, + style, + ...rest + } = props; + + const wrappedLongPress = () => { + longPressDetected = true; + onLongPress?.(); }; - private lastActive: boolean; - private longPressTimeout: ReturnType | undefined; - private longPressDetected: boolean; - - constructor(props: BaseButtonWithRefProps) { - super(props); - this.lastActive = false; - this.longPressDetected = false; - } + const onBegin = (e: CallbackEventType) => { + if (Platform.OS === 'android' && e.handlerData.pointerInside) { + longPressDetected = false; + if (onLongPress) { + longPressTimeout = setTimeout(wrappedLongPress, delayLongPress); + } + } - private handleEvent = ({ - nativeEvent, - }: HandlerStateChangeEvent) => { - const { state, oldState, pointerInside } = nativeEvent; - const active = pointerInside && state === State.ACTIVE; + lastActive = false; + }; - if (active !== this.lastActive && this.props.onActiveStateChange) { - this.props.onActiveStateChange(active); - } + const onStart = (e: CallbackEventType) => { + onActiveStateChange?.(true); - if ( - !this.longPressDetected && - oldState === State.ACTIVE && - state !== State.CANCELLED && - this.lastActive && - this.props.onPress - ) { - this.props.onPress(pointerInside); + if (Platform.OS !== 'android' && e.handlerData.pointerInside) { + longPressDetected = false; + if (onLongPress) { + longPressTimeout = setTimeout(wrappedLongPress, delayLongPress); + } } - if ( - !this.lastActive && - // NativeViewGestureHandler sends different events based on platform - state === (Platform.OS !== 'android' ? State.ACTIVE : State.BEGAN) && - pointerInside - ) { - this.longPressDetected = false; - if (this.props.onLongPress) { - this.longPressTimeout = setTimeout( - this.onLongPress, - this.props.delayLongPress - ); - } - } else if ( - // Cancel longpress timeout if it's set and the finger moved out of the view - state === State.ACTIVE && - !pointerInside && - this.longPressTimeout !== undefined - ) { - clearTimeout(this.longPressTimeout); - this.longPressTimeout = undefined; - } else if ( - // Cancel longpress timeout if it's set and the gesture has finished - this.longPressTimeout !== undefined && - (state === State.END || - state === State.CANCELLED || - state === State.FAILED) - ) { - clearTimeout(this.longPressTimeout); - this.longPressTimeout = undefined; + if (!e.handlerData.pointerInside && longPressTimeout !== undefined) { + clearTimeout(longPressTimeout); + longPressTimeout = undefined; } - this.lastActive = active; + lastActive = true; }; - private onLongPress = () => { - this.longPressDetected = true; - this.props.onLongPress?.(); - }; + const onEnd = (e: CallbackEventType, success: boolean) => { + if (!success) { + return; + } - // Normally, the parent would execute it's handler first, then forward the - // event to listeners. However, here our handler is virtually only forwarding - // events to listeners, so we reverse the order to keep the proper order of - // the callbacks (from "raw" ones to "processed"). - private onHandlerStateChange = ( - e: HandlerStateChangeEvent - ) => { - this.props.onHandlerStateChange?.(e); - this.handleEvent(e); - }; + if (!longPressDetected && onPress) { + onPress(e.handlerData.pointerInside); + } - private onGestureEvent = ( - e: GestureEvent - ) => { - this.props.onGestureEvent?.(e); - this.handleEvent( - e as HandlerStateChangeEvent - ); // TODO: maybe it is not correct + if (lastActive) { + onActiveStateChange?.(false); + } }; - override render() { - const { rippleColor, style, ...rest } = this.props; - - return ( - - ); - } -} - -const AnimatedInnerBaseButton = - Animated.createAnimatedComponent(InnerBaseButton); + const onFinalize = (_e: CallbackEventType) => { + if (lastActive) { + onActiveStateChange?.(false); + } -export const BaseButton = React.forwardRef< - React.ComponentType, - Omit ->((props, ref) => ); + if (longPressTimeout !== undefined) { + clearTimeout(longPressTimeout); + longPressTimeout = undefined; + } + lastActive = false; + }; -const AnimatedBaseButton = React.forwardRef< - React.ComponentType, - Animated.AnimatedProps ->((props, ref) => ); + return ( + + ); +}; const btnStyles = StyleSheet.create({ underlay: { @@ -158,102 +122,76 @@ const btnStyles = StyleSheet.create({ }, }); -class InnerRectButton extends React.Component { - static defaultProps = { - activeOpacity: 0.105, - underlayColor: 'black', - }; - - private opacity: Animated.Value; +export const RectButton = (props: RectButtonProps) => { + const activeOpacity = props.activeOpacity ?? 0.105; + const underlayColor = props.underlayColor ?? 'black'; - constructor(props: RectButtonWithRefProps) { - super(props); - this.opacity = new Animated.Value(0); - } + const opacity = useSharedValue(0); - private onActiveStateChange = (active: boolean) => { + const onActiveStateChange = (active: boolean) => { if (Platform.OS !== 'android') { - this.opacity.setValue(active ? this.props.activeOpacity! : 0); + opacity.value = active ? activeOpacity : 0; } - this.props.onActiveStateChange?.(active); + props.onActiveStateChange?.(active); }; - override render() { - const { children, style, ...rest } = this.props; - - const resolvedStyle = StyleSheet.flatten(style) ?? {}; - - return ( - - - {children} - - ); - } -} - -export const RectButton = React.forwardRef< - React.ComponentType, - Omit ->((props, ref) => ); - -class InnerBorderlessButton extends React.Component { - static defaultProps = { - activeOpacity: 0.3, - borderless: true, - }; - - private opacity: Animated.Value; + const { children, style, ...rest } = props; + + const resolvedStyle = StyleSheet.flatten(style ?? {}); + + const animatedStyle = useAnimatedStyle(() => { + return { + opacity: opacity.value, + }; + }); + + return ( + + + {children} + + ); +}; - constructor(props: BorderlessButtonWithRefProps) { - super(props); - this.opacity = new Animated.Value(1); - } +export const BorderlessButton = (props: BorderlessButtonProps) => { + const activeOpacity = props.activeOpacity ?? 0.3; + const opacity = useSharedValue(1); - private onActiveStateChange = (active: boolean) => { + const onActiveStateChange = (active: boolean) => { if (Platform.OS !== 'android') { - this.opacity.setValue(active ? this.props.activeOpacity! : 1); + opacity.value = active ? activeOpacity : 0; } - this.props.onActiveStateChange?.(active); + props.onActiveStateChange?.(active); }; - override render() { - const { children, style, innerRef, ...rest } = this.props; - - return ( - - {children} - - ); - } -} - -export const BorderlessButton = React.forwardRef< - React.ComponentType, - Omit ->((props, ref) => ); + const { children, style, ...rest } = props; + + return ( + + {children} + + ); +}; export { default as PureNativeButton } from './GestureHandlerButton'; diff --git a/packages/react-native-gesture-handler/src/components/GestureButtonsProps.ts b/packages/react-native-gesture-handler/src/components/GestureButtonsProps.ts index e9504bff32..f5ca99028b 100644 --- a/packages/react-native-gesture-handler/src/components/GestureButtonsProps.ts +++ b/packages/react-native-gesture-handler/src/components/GestureButtonsProps.ts @@ -91,7 +91,7 @@ export interface RawButtonProps testOnly_onLongPress?: Function | null; } interface ButtonWithRefProps { - innerRef?: React.ForwardedRef>; + ref?: React.RefObject; } export interface BaseButtonProps extends RawButtonProps { diff --git a/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx b/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx index 19dd2b4199..74860ae7e2 100644 --- a/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx +++ b/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx @@ -10,7 +10,7 @@ export default function createNativeWrapper

( config: Readonly = {} ) { const ComponentWrapper = ( - props: P & NativeWrapperProperties & { ref: React.RefObject } + props: P & NativeWrapperProperties & { ref?: React.RefObject } ) => { // Filter out props that should be passed to gesture handler wrapper const { gestureHandlerProps, childProps } = Object.keys(props).reduce( diff --git a/packages/react-native-gesture-handler/src/v3/hooks/gestures/native/useNative.ts b/packages/react-native-gesture-handler/src/v3/hooks/gestures/native/useNative.ts index 0ca8d96d5b..483ca34f03 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/gestures/native/useNative.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/gestures/native/useNative.ts @@ -11,7 +11,7 @@ import { useGesture } from '../../useGesture'; import { cloneConfig } from '../../utils'; import { NativeGestureNativeProperties } from './NativeProperties'; -type NativeViewHandlerData = { +export type NativeViewHandlerData = { pointerInside: boolean; }; diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts index e6c137c6d6..9f8637db13 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts @@ -82,4 +82,5 @@ export const NativeWrapperProps = new Set([ ...HandlerCallbacks, ...NativeHandlerNativeProperties, ...ExternalRelationsConfig, + 'disableReanimated', ]); diff --git a/packages/react-native-gesture-handler/src/v3/types/NativeWrapperType.ts b/packages/react-native-gesture-handler/src/v3/types/NativeWrapperType.ts index 35684b885c..0f38ab57c3 100644 --- a/packages/react-native-gesture-handler/src/v3/types/NativeWrapperType.ts +++ b/packages/react-native-gesture-handler/src/v3/types/NativeWrapperType.ts @@ -1,7 +1,8 @@ import { CommonGestureConfig, ExternalRelations, GestureCallbacks } from '.'; import { NativeGestureNativeProperties } from '../hooks/gestures/native/NativeProperties'; +import { NativeViewHandlerData } from '../hooks/gestures/native/useNative'; export type NativeWrapperProperties = CommonGestureConfig & - GestureCallbacks & + GestureCallbacks & NativeGestureNativeProperties & ExternalRelations; From 8ccf267ad7101105e946d9bba29b303f01a88287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Mon, 3 Nov 2025 16:30:49 +0100 Subject: [PATCH 04/21] Remove Reanimated --- .../src/components/GestureButtons.tsx | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/packages/react-native-gesture-handler/src/components/GestureButtons.tsx b/packages/react-native-gesture-handler/src/components/GestureButtons.tsx index d2f019ecd5..d8578c9ba5 100644 --- a/packages/react-native-gesture-handler/src/components/GestureButtons.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureButtons.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Platform, StyleSheet } from 'react-native'; +import { Platform, StyleSheet, Animated } from 'react-native'; import createNativeWrapper from '../v3/createNativeWrapper'; import GestureHandlerButton from './GestureHandlerButton'; @@ -8,10 +8,7 @@ import type { BorderlessButtonProps, RectButtonProps, } from './GestureButtonsProps'; -import Animated, { - useAnimatedStyle, - useSharedValue, -} from 'react-native-reanimated'; + import type { GestureStateChangeEvent } from '../v3/types'; import type { NativeViewHandlerData } from '../v3/hooks/gestures/native/useNative'; @@ -126,11 +123,11 @@ export const RectButton = (props: RectButtonProps) => { const activeOpacity = props.activeOpacity ?? 0.105; const underlayColor = props.underlayColor ?? 'black'; - const opacity = useSharedValue(0); + const opacity = new Animated.Value(0); const onActiveStateChange = (active: boolean) => { if (Platform.OS !== 'android') { - opacity.value = active ? activeOpacity : 0; + opacity.setValue(active ? activeOpacity : 0); } props.onActiveStateChange?.(active); @@ -140,12 +137,6 @@ export const RectButton = (props: RectButtonProps) => { const resolvedStyle = StyleSheet.flatten(style ?? {}); - const animatedStyle = useAnimatedStyle(() => { - return { - opacity: opacity.value, - }; - }); - return ( { style={[ btnStyles.underlay, { + opacity, backgroundColor: underlayColor, borderRadius: resolvedStyle.borderRadius, borderTopLeftRadius: resolvedStyle.borderTopLeftRadius, @@ -162,7 +154,6 @@ export const RectButton = (props: RectButtonProps) => { borderBottomLeftRadius: resolvedStyle.borderBottomLeftRadius, borderBottomRightRadius: resolvedStyle.borderBottomRightRadius, }, - animatedStyle, ]} /> {children} @@ -172,11 +163,11 @@ export const RectButton = (props: RectButtonProps) => { export const BorderlessButton = (props: BorderlessButtonProps) => { const activeOpacity = props.activeOpacity ?? 0.3; - const opacity = useSharedValue(1); + const opacity = new Animated.Value(1); const onActiveStateChange = (active: boolean) => { if (Platform.OS !== 'android') { - opacity.value = active ? activeOpacity : 0; + opacity.setValue(active ? activeOpacity : 0); } props.onActiveStateChange?.(active); @@ -188,7 +179,7 @@ export const BorderlessButton = (props: BorderlessButtonProps) => { + style={[style, Platform.OS === 'ios' && { opacity }]}> {children} ); From df3f1fb02d1f001688a0c8319c4861eabda1a7f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Tue, 4 Nov 2025 10:46:43 +0100 Subject: [PATCH 05/21] Components --- .../src/components/GestureComponents.tsx | 77 ++++++++++++------- .../src/v3/createNativeWrapper.tsx | 20 ++++- .../src/v3/types/GestureTypes.ts | 6 +- 3 files changed, 73 insertions(+), 30 deletions(-) diff --git a/packages/react-native-gesture-handler/src/components/GestureComponents.tsx b/packages/react-native-gesture-handler/src/components/GestureComponents.tsx index 8bdf73a35d..8f433d0d45 100644 --- a/packages/react-native-gesture-handler/src/components/GestureComponents.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureComponents.tsx @@ -19,19 +19,19 @@ import { RefreshControl as RNRefreshControl, } from 'react-native'; -import createNativeWrapper from '../handlers/createNativeWrapper'; +import createNativeWrapper, { + ComponentWrapperRef, +} from '../v3/createNativeWrapper'; -import { - NativeViewGestureHandlerProps, - nativeViewProps, -} from '../handlers/NativeViewGestureHandler'; - -import { toArray } from '../utils'; +import { NativeWrapperProperties } from '../v3/types/NativeWrapperType'; +import { NativeWrapperProps } from '../v3/hooks/utils'; +import { AnyGesture } from '../v3/types'; export const RefreshControl = createNativeWrapper(RNRefreshControl, { disallowInterruption: true, shouldCancelWhenOutside: false, }); + // eslint-disable-next-line @typescript-eslint/no-redeclare export type RefreshControl = typeof RefreshControl & RNRefreshControl; @@ -42,31 +42,43 @@ const GHScrollView = createNativeWrapper>( shouldCancelWhenOutside: false, } ); -export const ScrollView = React.forwardRef< - RNScrollView, - RNScrollViewProps & NativeViewGestureHandlerProps ->((props, ref) => { - const refreshControlGestureRef = React.useRef(null); - const { refreshControl, waitFor, ...rest } = props; +export const ScrollView = ( + props: RNScrollViewProps & NativeWrapperProperties +) => { + const refreshControlRef = + React.useRef>(null); + const { refreshControl, requireExternalGestureToFail, ...rest } = props; + + const waitFor = []; + + if (Array.isArray(requireExternalGestureToFail)) { + waitFor.push(...requireExternalGestureToFail); + } else if (requireExternalGestureToFail) { + waitFor.push(requireExternalGestureToFail); + } + + if (refreshControlRef.current) { + waitFor.push(refreshControlRef.current.gestureRef); + } return ( ); -}); +}; // Backward type compatibility with https://github.com/software-mansion/react-native-gesture-handler/blob/db78d3ca7d48e8ba57482d3fe9b0a15aa79d9932/react-native-gesture-handler.d.ts#L440-L457 // include methods of wrapped components by creating an intersection type with the RN component instead of duplicating them. // eslint-disable-next-line @typescript-eslint/no-redeclare @@ -91,16 +103,17 @@ export const DrawerLayoutAndroid = createNativeWrapper< export type DrawerLayoutAndroid = typeof DrawerLayoutAndroid & RNDrawerLayoutAndroid; -export const FlatList = React.forwardRef((props, ref) => { - const refreshControlGestureRef = React.useRef(null); +export const FlatList = ((props) => { + const refreshControlRef = + React.useRef>(null); - const { waitFor, refreshControl, ...rest } = props; + const { requireExternalGestureToFail, refreshControl, ...rest } = props; const flatListProps = {}; const scrollViewProps = {}; for (const [propName, value] of Object.entries(rest)) { - // https://github.com/microsoft/TypeScript/issues/26255 - if ((nativeViewProps as readonly string[]).includes(propName)) { + // @ts-ignore https://github.com/microsoft/TypeScript/issues/26255 + if (NativeWrapperProps.has(propName)) { // @ts-ignore - this function cannot have generic type so we have to ignore this error // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment scrollViewProps[propName] = value; @@ -111,17 +124,29 @@ export const FlatList = React.forwardRef((props, ref) => { } } + const waitFor: AnyGesture[] = []; + + if (Array.isArray(requireExternalGestureToFail)) { + waitFor.push(...requireExternalGestureToFail); + } else if (requireExternalGestureToFail) { + waitFor.push(requireExternalGestureToFail); + } + + if (refreshControlRef.current) { + waitFor.push(refreshControlRef.current.gestureRef); + } + return ( // @ts-ignore - this function cannot have generic type so we have to ignore this error ( )} @@ -130,7 +155,7 @@ export const FlatList = React.forwardRef((props, ref) => { refreshControl ? React.cloneElement(refreshControl, { // @ts-ignore for reasons unknown to me, `ref` doesn't exist on the type inferred by TS - ref: refreshControlGestureRef, + ref: refreshControlRef, }) : undefined } @@ -140,7 +165,7 @@ export const FlatList = React.forwardRef((props, ref) => { props: PropsWithChildren< Omit, 'renderScrollComponent'> & RefAttributes> & - NativeViewGestureHandlerProps + NativeWrapperProperties >, ref?: ForwardedRef> ) => ReactElement | null; diff --git a/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx b/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx index 74860ae7e2..048ffa89a6 100644 --- a/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx +++ b/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx @@ -4,6 +4,16 @@ import { NativeWrapperProps } from './hooks/utils'; import { useNative } from './hooks/gestures'; import { NativeDetector } from './NativeDetector/NativeDetector'; import type { NativeWrapperProperties } from './types/NativeWrapperType'; +import { + NativeViewGestureConfig, + NativeViewHandlerData, +} from './hooks/gestures/native/useNative'; +import { Gesture } from './types'; + +export type ComponentWrapperRef

= { + componentRef: React.ComponentType

; + gestureRef: Gesture; +}; export default function createNativeWrapper

( Component: React.ComponentType

, @@ -37,9 +47,17 @@ export default function createNativeWrapper

( const native = useNative(gestureHandlerProps); + const componentRef = React.useRef>(null); + const gestureRef = React.useRef(native); + + React.useImperativeHandle(props.ref, () => ({ + componentRef, + gestureRef, + })); + return ( - + ); }; diff --git a/packages/react-native-gesture-handler/src/v3/types/GestureTypes.ts b/packages/react-native-gesture-handler/src/v3/types/GestureTypes.ts index 00d4259419..0373811dfe 100644 --- a/packages/react-native-gesture-handler/src/v3/types/GestureTypes.ts +++ b/packages/react-native-gesture-handler/src/v3/types/GestureTypes.ts @@ -10,9 +10,9 @@ import { FilterNeverProperties } from './UtilityTypes'; // Unfortunately, this type cannot be moved into ConfigTypes.ts because of circular dependency export type ExternalRelations = { - simultaneousWithExternalGesture?: Gesture | Gesture[]; - requireExternalGestureToFail?: Gesture | Gesture[]; - blocksExternalGesture?: Gesture | Gesture[]; + simultaneousWithExternalGesture?: AnyGesture | AnyGesture[]; + requireExternalGestureToFail?: AnyGesture | AnyGesture[]; + blocksExternalGesture?: AnyGesture | AnyGesture[]; }; // Similarly, this type cannot be moved into ConfigTypes.ts because it depends on `ExternalRelations` From a1da35a8802be3187ef2091ecddd87b725a4cd8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Tue, 4 Nov 2025 10:49:50 +0100 Subject: [PATCH 06/21] Add proper values to relations --- .../src/v3/hooks/utils/propsWhiteList.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts index 9f8637db13..30e83c9548 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts @@ -26,7 +26,11 @@ const CommonConfig = new Set([ 'touchAction', ]); -const ExternalRelationsConfig = new Set([]); +const ExternalRelationsConfig = new Set([ + 'simultaneousWithExternalGesture', + 'requireExternalGestureToFail', + 'blocksExternalGesture', +]); export const allowedNativeProps = new Set< keyof CommonGestureConfig | keyof InternalConfigProps From a6804613321401efa83731156b8fb42c2608b4d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Tue, 4 Nov 2025 11:23:59 +0100 Subject: [PATCH 07/21] Bring back old components --- .../src/components/GestureButtons.tsx | 335 +++++++++++------- .../src/components/GestureButtonsProps.ts | 2 +- .../src/components/GestureComponents.tsx | 77 ++-- .../src/v3/components/GestureButtons.tsx | 187 ++++++++++ .../src/v3/components/GestureButtonsProps.ts | 156 ++++++++ .../src/v3/components/GestureComponents.tsx | 173 +++++++++ 6 files changed, 746 insertions(+), 184 deletions(-) create mode 100644 packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx create mode 100644 packages/react-native-gesture-handler/src/v3/components/GestureButtonsProps.ts create mode 100644 packages/react-native-gesture-handler/src/v3/components/GestureComponents.tsx diff --git a/packages/react-native-gesture-handler/src/components/GestureButtons.tsx b/packages/react-native-gesture-handler/src/components/GestureButtons.tsx index d8578c9ba5..04409f7f2c 100644 --- a/packages/react-native-gesture-handler/src/components/GestureButtons.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureButtons.tsx @@ -1,113 +1,152 @@ -import React from 'react'; -import { Platform, StyleSheet, Animated } from 'react-native'; +import * as React from 'react'; +import { Animated, Platform, StyleSheet } from 'react-native'; -import createNativeWrapper from '../v3/createNativeWrapper'; +import createNativeWrapper from '../handlers/createNativeWrapper'; import GestureHandlerButton from './GestureHandlerButton'; +import { State } from '../State'; + +import { + GestureEvent, + HandlerStateChangeEvent, +} from '../handlers/gestureHandlerCommon'; +import type { NativeViewGestureHandlerPayload } from '../handlers/GestureHandlerEventPayload'; import type { + BaseButtonWithRefProps, BaseButtonProps, - BorderlessButtonProps, + RectButtonWithRefProps, RectButtonProps, + BorderlessButtonWithRefProps, + BorderlessButtonProps, } from './GestureButtonsProps'; -import type { GestureStateChangeEvent } from '../v3/types'; -import type { NativeViewHandlerData } from '../v3/hooks/gestures/native/useNative'; - -type CallbackEventType = GestureStateChangeEvent; - export const RawButton = createNativeWrapper(GestureHandlerButton, { shouldCancelWhenOutside: false, shouldActivateOnStart: false, }); -export const BaseButton = (props: BaseButtonProps) => { - let lastActive: boolean; - let longPressDetected: boolean; - let longPressTimeout: ReturnType | undefined; - - const delayLongPress = props.delayLongPress ?? 600; - - const { - onLongPress, - onPress, - onActiveStateChange, - rippleColor, - style, - ...rest - } = props; - - const wrappedLongPress = () => { - longPressDetected = true; - onLongPress?.(); +class InnerBaseButton extends React.Component { + static defaultProps = { + delayLongPress: 600, }; - const onBegin = (e: CallbackEventType) => { - if (Platform.OS === 'android' && e.handlerData.pointerInside) { - longPressDetected = false; - if (onLongPress) { - longPressTimeout = setTimeout(wrappedLongPress, delayLongPress); - } - } + private lastActive: boolean; + private longPressTimeout: ReturnType | undefined; + private longPressDetected: boolean; - lastActive = false; - }; + constructor(props: BaseButtonWithRefProps) { + super(props); + this.lastActive = false; + this.longPressDetected = false; + } - const onStart = (e: CallbackEventType) => { - onActiveStateChange?.(true); + private handleEvent = ({ + nativeEvent, + }: HandlerStateChangeEvent) => { + const { state, oldState, pointerInside } = nativeEvent; + const active = pointerInside && state === State.ACTIVE; - if (Platform.OS !== 'android' && e.handlerData.pointerInside) { - longPressDetected = false; - if (onLongPress) { - longPressTimeout = setTimeout(wrappedLongPress, delayLongPress); - } + if (active !== this.lastActive && this.props.onActiveStateChange) { + this.props.onActiveStateChange(active); } - if (!e.handlerData.pointerInside && longPressTimeout !== undefined) { - clearTimeout(longPressTimeout); - longPressTimeout = undefined; + if ( + !this.longPressDetected && + oldState === State.ACTIVE && + state !== State.CANCELLED && + this.lastActive && + this.props.onPress + ) { + this.props.onPress(pointerInside); } - lastActive = true; - }; - - const onEnd = (e: CallbackEventType, success: boolean) => { - if (!success) { - return; + if ( + !this.lastActive && + // NativeViewGestureHandler sends different events based on platform + state === (Platform.OS !== 'android' ? State.ACTIVE : State.BEGAN) && + pointerInside + ) { + this.longPressDetected = false; + if (this.props.onLongPress) { + this.longPressTimeout = setTimeout( + this.onLongPress, + this.props.delayLongPress + ); + } + } else if ( + // Cancel longpress timeout if it's set and the finger moved out of the view + state === State.ACTIVE && + !pointerInside && + this.longPressTimeout !== undefined + ) { + clearTimeout(this.longPressTimeout); + this.longPressTimeout = undefined; + } else if ( + // Cancel longpress timeout if it's set and the gesture has finished + this.longPressTimeout !== undefined && + (state === State.END || + state === State.CANCELLED || + state === State.FAILED) + ) { + clearTimeout(this.longPressTimeout); + this.longPressTimeout = undefined; } - if (!longPressDetected && onPress) { - onPress(e.handlerData.pointerInside); - } + this.lastActive = active; + }; - if (lastActive) { - onActiveStateChange?.(false); - } + private onLongPress = () => { + this.longPressDetected = true; + this.props.onLongPress?.(); }; - const onFinalize = (_e: CallbackEventType) => { - if (lastActive) { - onActiveStateChange?.(false); - } + // Normally, the parent would execute it's handler first, then forward the + // event to listeners. However, here our handler is virtually only forwarding + // events to listeners, so we reverse the order to keep the proper order of + // the callbacks (from "raw" ones to "processed"). + private onHandlerStateChange = ( + e: HandlerStateChangeEvent + ) => { + this.props.onHandlerStateChange?.(e); + this.handleEvent(e); + }; - if (longPressTimeout !== undefined) { - clearTimeout(longPressTimeout); - longPressTimeout = undefined; - } - lastActive = false; + private onGestureEvent = ( + e: GestureEvent + ) => { + this.props.onGestureEvent?.(e); + this.handleEvent( + e as HandlerStateChangeEvent + ); // TODO: maybe it is not correct }; - return ( - - ); -}; + override render() { + const { rippleColor, style, ...rest } = this.props; + + return ( + + ); + } +} + +const AnimatedInnerBaseButton = + Animated.createAnimatedComponent(InnerBaseButton); + +export const BaseButton = React.forwardRef< + React.ComponentType, + Omit +>((props, ref) => ); + +const AnimatedBaseButton = React.forwardRef< + React.ComponentType, + Animated.AnimatedProps +>((props, ref) => ); const btnStyles = StyleSheet.create({ underlay: { @@ -119,70 +158,102 @@ const btnStyles = StyleSheet.create({ }, }); -export const RectButton = (props: RectButtonProps) => { - const activeOpacity = props.activeOpacity ?? 0.105; - const underlayColor = props.underlayColor ?? 'black'; +class InnerRectButton extends React.Component { + static defaultProps = { + activeOpacity: 0.105, + underlayColor: 'black', + }; + + private opacity: Animated.Value; - const opacity = new Animated.Value(0); + constructor(props: RectButtonWithRefProps) { + super(props); + this.opacity = new Animated.Value(0); + } - const onActiveStateChange = (active: boolean) => { + private onActiveStateChange = (active: boolean) => { if (Platform.OS !== 'android') { - opacity.setValue(active ? activeOpacity : 0); + this.opacity.setValue(active ? this.props.activeOpacity! : 0); } - props.onActiveStateChange?.(active); + this.props.onActiveStateChange?.(active); }; - const { children, style, ...rest } = props; - - const resolvedStyle = StyleSheet.flatten(style ?? {}); - - return ( - - - {children} - - ); -}; + override render() { + const { children, style, ...rest } = this.props; + + const resolvedStyle = StyleSheet.flatten(style) ?? {}; + + return ( + + + {children} + + ); + } +} + +export const RectButton = React.forwardRef< + React.ComponentType, + Omit +>((props, ref) => ); + +class InnerBorderlessButton extends React.Component { + static defaultProps = { + activeOpacity: 0.3, + borderless: true, + }; + + private opacity: Animated.Value; -export const BorderlessButton = (props: BorderlessButtonProps) => { - const activeOpacity = props.activeOpacity ?? 0.3; - const opacity = new Animated.Value(1); + constructor(props: BorderlessButtonWithRefProps) { + super(props); + this.opacity = new Animated.Value(1); + } - const onActiveStateChange = (active: boolean) => { + private onActiveStateChange = (active: boolean) => { if (Platform.OS !== 'android') { - opacity.setValue(active ? activeOpacity : 0); + this.opacity.setValue(active ? this.props.activeOpacity! : 1); } - props.onActiveStateChange?.(active); + this.props.onActiveStateChange?.(active); }; - const { children, style, ...rest } = props; - - return ( - - {children} - - ); -}; + override render() { + const { children, style, innerRef, ...rest } = this.props; + + return ( + + {children} + + ); + } +} + +export const BorderlessButton = React.forwardRef< + React.ComponentType, + Omit +>((props, ref) => ); export { default as PureNativeButton } from './GestureHandlerButton'; diff --git a/packages/react-native-gesture-handler/src/components/GestureButtonsProps.ts b/packages/react-native-gesture-handler/src/components/GestureButtonsProps.ts index f5ca99028b..e9504bff32 100644 --- a/packages/react-native-gesture-handler/src/components/GestureButtonsProps.ts +++ b/packages/react-native-gesture-handler/src/components/GestureButtonsProps.ts @@ -91,7 +91,7 @@ export interface RawButtonProps testOnly_onLongPress?: Function | null; } interface ButtonWithRefProps { - ref?: React.RefObject; + innerRef?: React.ForwardedRef>; } export interface BaseButtonProps extends RawButtonProps { diff --git a/packages/react-native-gesture-handler/src/components/GestureComponents.tsx b/packages/react-native-gesture-handler/src/components/GestureComponents.tsx index 8f433d0d45..8bdf73a35d 100644 --- a/packages/react-native-gesture-handler/src/components/GestureComponents.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureComponents.tsx @@ -19,19 +19,19 @@ import { RefreshControl as RNRefreshControl, } from 'react-native'; -import createNativeWrapper, { - ComponentWrapperRef, -} from '../v3/createNativeWrapper'; +import createNativeWrapper from '../handlers/createNativeWrapper'; -import { NativeWrapperProperties } from '../v3/types/NativeWrapperType'; -import { NativeWrapperProps } from '../v3/hooks/utils'; -import { AnyGesture } from '../v3/types'; +import { + NativeViewGestureHandlerProps, + nativeViewProps, +} from '../handlers/NativeViewGestureHandler'; + +import { toArray } from '../utils'; export const RefreshControl = createNativeWrapper(RNRefreshControl, { disallowInterruption: true, shouldCancelWhenOutside: false, }); - // eslint-disable-next-line @typescript-eslint/no-redeclare export type RefreshControl = typeof RefreshControl & RNRefreshControl; @@ -42,43 +42,31 @@ const GHScrollView = createNativeWrapper>( shouldCancelWhenOutside: false, } ); -export const ScrollView = ( - props: RNScrollViewProps & NativeWrapperProperties -) => { - const refreshControlRef = - React.useRef>(null); - const { refreshControl, requireExternalGestureToFail, ...rest } = props; - - const waitFor = []; - - if (Array.isArray(requireExternalGestureToFail)) { - waitFor.push(...requireExternalGestureToFail); - } else if (requireExternalGestureToFail) { - waitFor.push(requireExternalGestureToFail); - } - - if (refreshControlRef.current) { - waitFor.push(refreshControlRef.current.gestureRef); - } +export const ScrollView = React.forwardRef< + RNScrollView, + RNScrollViewProps & NativeViewGestureHandlerProps +>((props, ref) => { + const refreshControlGestureRef = React.useRef(null); + const { refreshControl, waitFor, ...rest } = props; return ( ); -}; +}); // Backward type compatibility with https://github.com/software-mansion/react-native-gesture-handler/blob/db78d3ca7d48e8ba57482d3fe9b0a15aa79d9932/react-native-gesture-handler.d.ts#L440-L457 // include methods of wrapped components by creating an intersection type with the RN component instead of duplicating them. // eslint-disable-next-line @typescript-eslint/no-redeclare @@ -103,17 +91,16 @@ export const DrawerLayoutAndroid = createNativeWrapper< export type DrawerLayoutAndroid = typeof DrawerLayoutAndroid & RNDrawerLayoutAndroid; -export const FlatList = ((props) => { - const refreshControlRef = - React.useRef>(null); +export const FlatList = React.forwardRef((props, ref) => { + const refreshControlGestureRef = React.useRef(null); - const { requireExternalGestureToFail, refreshControl, ...rest } = props; + const { waitFor, refreshControl, ...rest } = props; const flatListProps = {}; const scrollViewProps = {}; for (const [propName, value] of Object.entries(rest)) { - // @ts-ignore https://github.com/microsoft/TypeScript/issues/26255 - if (NativeWrapperProps.has(propName)) { + // https://github.com/microsoft/TypeScript/issues/26255 + if ((nativeViewProps as readonly string[]).includes(propName)) { // @ts-ignore - this function cannot have generic type so we have to ignore this error // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment scrollViewProps[propName] = value; @@ -124,29 +111,17 @@ export const FlatList = ((props) => { } } - const waitFor: AnyGesture[] = []; - - if (Array.isArray(requireExternalGestureToFail)) { - waitFor.push(...requireExternalGestureToFail); - } else if (requireExternalGestureToFail) { - waitFor.push(requireExternalGestureToFail); - } - - if (refreshControlRef.current) { - waitFor.push(refreshControlRef.current.gestureRef); - } - return ( // @ts-ignore - this function cannot have generic type so we have to ignore this error ( )} @@ -155,7 +130,7 @@ export const FlatList = ((props) => { refreshControl ? React.cloneElement(refreshControl, { // @ts-ignore for reasons unknown to me, `ref` doesn't exist on the type inferred by TS - ref: refreshControlRef, + ref: refreshControlGestureRef, }) : undefined } @@ -165,7 +140,7 @@ export const FlatList = ((props) => { props: PropsWithChildren< Omit, 'renderScrollComponent'> & RefAttributes> & - NativeWrapperProperties + NativeViewGestureHandlerProps >, ref?: ForwardedRef> ) => ReactElement | null; diff --git a/packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx b/packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx new file mode 100644 index 0000000000..8821af2e55 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx @@ -0,0 +1,187 @@ +import React from 'react'; +import { Platform, StyleSheet, Animated } from 'react-native'; +import createNativeWrapper from '../createNativeWrapper'; +import GestureHandlerButton from '../../components/GestureHandlerButton'; +import type { + BaseButtonProps, + BorderlessButtonProps, + RectButtonProps, +} from './GestureButtonsProps'; + +import type { GestureStateChangeEvent } from '../types'; +import type { NativeViewHandlerData } from '../hooks/gestures/native/useNative'; + +type CallbackEventType = GestureStateChangeEvent; + +export const RawButton = createNativeWrapper(GestureHandlerButton, { + shouldCancelWhenOutside: false, + shouldActivateOnStart: false, +}); + +export const BaseButton = (props: BaseButtonProps) => { + let lastActive: boolean; + let longPressDetected: boolean; + let longPressTimeout: ReturnType | undefined; + + const delayLongPress = props.delayLongPress ?? 600; + + const { + onLongPress, + onPress, + onActiveStateChange, + rippleColor, + style, + ...rest + } = props; + + const wrappedLongPress = () => { + longPressDetected = true; + onLongPress?.(); + }; + + const onBegin = (e: CallbackEventType) => { + if (Platform.OS === 'android' && e.handlerData.pointerInside) { + longPressDetected = false; + if (onLongPress) { + longPressTimeout = setTimeout(wrappedLongPress, delayLongPress); + } + } + + lastActive = false; + }; + + const onStart = (e: CallbackEventType) => { + onActiveStateChange?.(true); + + if (Platform.OS !== 'android' && e.handlerData.pointerInside) { + longPressDetected = false; + if (onLongPress) { + longPressTimeout = setTimeout(wrappedLongPress, delayLongPress); + } + } + + if (!e.handlerData.pointerInside && longPressTimeout !== undefined) { + clearTimeout(longPressTimeout); + longPressTimeout = undefined; + } + + lastActive = true; + }; + + const onEnd = (e: CallbackEventType, success: boolean) => { + if (!success) { + return; + } + + if (!longPressDetected && onPress) { + onPress(e.handlerData.pointerInside); + } + + if (lastActive) { + onActiveStateChange?.(false); + } + }; + + const onFinalize = (_e: CallbackEventType) => { + if (lastActive) { + onActiveStateChange?.(false); + } + + if (longPressTimeout !== undefined) { + clearTimeout(longPressTimeout); + longPressTimeout = undefined; + } + lastActive = false; + }; + + return ( + + ); +}; + +const btnStyles = StyleSheet.create({ + underlay: { + position: 'absolute', + left: 0, + right: 0, + bottom: 0, + top: 0, + }, +}); + +export const RectButton = (props: RectButtonProps) => { + const activeOpacity = props.activeOpacity ?? 0.105; + const underlayColor = props.underlayColor ?? 'black'; + + const opacity = new Animated.Value(0); + + const onActiveStateChange = (active: boolean) => { + if (Platform.OS !== 'android') { + opacity.setValue(active ? activeOpacity : 0); + } + + props.onActiveStateChange?.(active); + }; + + const { children, style, ...rest } = props; + + const resolvedStyle = StyleSheet.flatten(style ?? {}); + + return ( + + + {children} + + ); +}; + +export const BorderlessButton = (props: BorderlessButtonProps) => { + const activeOpacity = props.activeOpacity ?? 0.3; + const opacity = new Animated.Value(1); + + const onActiveStateChange = (active: boolean) => { + if (Platform.OS !== 'android') { + opacity.setValue(active ? activeOpacity : 0); + } + + props.onActiveStateChange?.(active); + }; + + const { children, style, ...rest } = props; + + return ( + + {children} + + ); +}; + +export { default as PureNativeButton } from '../../components/GestureHandlerButton'; diff --git a/packages/react-native-gesture-handler/src/v3/components/GestureButtonsProps.ts b/packages/react-native-gesture-handler/src/v3/components/GestureButtonsProps.ts new file mode 100644 index 0000000000..20122247b3 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/components/GestureButtonsProps.ts @@ -0,0 +1,156 @@ +import * as React from 'react'; +import { + AccessibilityProps, + ColorValue, + LayoutChangeEvent, + StyleProp, + ViewStyle, +} from 'react-native'; +import type { NativeViewGestureHandlerProps } from '../../handlers/NativeViewGestureHandler'; + +export interface RawButtonProps + extends NativeViewGestureHandlerProps, + AccessibilityProps { + /** + * Defines if more than one button could be pressed simultaneously. By default + * set true. + */ + exclusive?: boolean; + // TODO: we should transform props in `createNativeWrapper` + /** + * Android only. + * + * Defines color of native ripple animation used since API level 21. + */ + rippleColor?: number | ColorValue | null; + + /** + * Android only. + * + * Defines radius of native ripple animation used since API level 21. + */ + rippleRadius?: number | null; + + /** + * Android only. + * + * Set this to true if you want the ripple animation to render outside the view bounds. + */ + borderless?: boolean; + + /** + * Android only. + * + * Defines whether the ripple animation should be drawn on the foreground of the view. + */ + foreground?: boolean; + + /** + * Android only. + * + * Set this to true if you don't want the system to play sound when the button is pressed. + */ + touchSoundDisabled?: boolean; + + /** + * Style object, use it to set additional styles. + */ + style?: StyleProp; + + /** + * Invoked on mount and layout changes. + */ + onLayout?: (event: LayoutChangeEvent) => void; + + /** + * Used for testing-library compatibility, not passed to the native component. + * @deprecated test-only props are deprecated and will be removed in the future. + */ + // eslint-disable-next-line @typescript-eslint/ban-types + testOnly_onPress?: Function | null; + + /** + * Used for testing-library compatibility, not passed to the native component. + * @deprecated test-only props are deprecated and will be removed in the future. + */ + // eslint-disable-next-line @typescript-eslint/ban-types + testOnly_onPressIn?: Function | null; + + /** + * Used for testing-library compatibility, not passed to the native component. + * @deprecated test-only props are deprecated and will be removed in the future. + */ + // eslint-disable-next-line @typescript-eslint/ban-types + testOnly_onPressOut?: Function | null; + + /** + * Used for testing-library compatibility, not passed to the native component. + * @deprecated test-only props are deprecated and will be removed in the future. + */ + // eslint-disable-next-line @typescript-eslint/ban-types + testOnly_onLongPress?: Function | null; +} +interface ButtonWithRefProps { + ref?: React.RefObject; +} + +export interface BaseButtonProps extends RawButtonProps { + /** + * Called when the button gets pressed (analogous to `onPress` in + * `TouchableHighlight` from RN core). + */ + onPress?: (pointerInside: boolean) => void; + + /** + * Called when the button gets pressed and is held for `delayLongPress` + * milliseconds. + */ + onLongPress?: () => void; + + /** + * Called when button changes from inactive to active and vice versa. It + * passes active state as a boolean variable as a first parameter for that + * method. + */ + onActiveStateChange?: (active: boolean) => void; + style?: StyleProp; + testID?: string; + + /** + * Delay, in milliseconds, after which the `onLongPress` callback gets called. + * Defaults to 600. + */ + delayLongPress?: number; +} +export interface BaseButtonWithRefProps + extends BaseButtonProps, + ButtonWithRefProps {} + +export interface RectButtonProps extends BaseButtonProps { + /** + * Background color that will be dimmed when button is in active state. + */ + underlayColor?: string; + + /** + * iOS only. + * + * Opacity applied to the underlay when button is in active state. + */ + activeOpacity?: number; +} +export interface RectButtonWithRefProps + extends RectButtonProps, + ButtonWithRefProps {} + +export interface BorderlessButtonProps extends BaseButtonProps { + /** + * iOS only. + * + * Opacity applied to the button when it is in an active state. + */ + activeOpacity?: number; +} +export interface BorderlessButtonWithRefProps + extends BorderlessButtonProps, + ButtonWithRefProps {} diff --git a/packages/react-native-gesture-handler/src/v3/components/GestureComponents.tsx b/packages/react-native-gesture-handler/src/v3/components/GestureComponents.tsx new file mode 100644 index 0000000000..ad7a33358b --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/components/GestureComponents.tsx @@ -0,0 +1,173 @@ +import * as React from 'react'; +import { + PropsWithChildren, + ForwardedRef, + RefAttributes, + ReactElement, +} from 'react'; +import { + ScrollView as RNScrollView, + ScrollViewProps as RNScrollViewProps, + Switch as RNSwitch, + SwitchProps as RNSwitchProps, + TextInput as RNTextInput, + TextInputProps as RNTextInputProps, + DrawerLayoutAndroid as RNDrawerLayoutAndroid, + DrawerLayoutAndroidProps as RNDrawerLayoutAndroidProps, + FlatList as RNFlatList, + FlatListProps as RNFlatListProps, + RefreshControl as RNRefreshControl, +} from 'react-native'; + +import createNativeWrapper, { + ComponentWrapperRef, +} from '../createNativeWrapper'; + +import { NativeWrapperProperties } from '../types/NativeWrapperType'; +import { NativeWrapperProps } from '../hooks/utils'; +import { AnyGesture } from '../types'; + +export const RefreshControl = createNativeWrapper(RNRefreshControl, { + disallowInterruption: true, + shouldCancelWhenOutside: false, +}); + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type RefreshControl = typeof RefreshControl & RNRefreshControl; + +const GHScrollView = createNativeWrapper>( + RNScrollView, + { + disallowInterruption: true, + shouldCancelWhenOutside: false, + } +); +export const ScrollView = ( + props: RNScrollViewProps & NativeWrapperProperties +) => { + const refreshControlRef = + React.useRef>(null); + const { refreshControl, requireExternalGestureToFail, ...rest } = props; + + const waitFor = []; + + if (Array.isArray(requireExternalGestureToFail)) { + waitFor.push(...requireExternalGestureToFail); + } else if (requireExternalGestureToFail) { + waitFor.push(requireExternalGestureToFail); + } + + if (refreshControlRef.current) { + waitFor.push(refreshControlRef.current.gestureRef); + } + + return ( + + ); +}; +// Backward type compatibility with https://github.com/software-mansion/react-native-gesture-handler/blob/db78d3ca7d48e8ba57482d3fe9b0a15aa79d9932/react-native-gesture-handler.d.ts#L440-L457 +// include methods of wrapped components by creating an intersection type with the RN component instead of duplicating them. +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type ScrollView = typeof GHScrollView & RNScrollView; + +export const Switch = createNativeWrapper(RNSwitch, { + shouldCancelWhenOutside: false, + shouldActivateOnStart: true, + disallowInterruption: true, +}); +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type Switch = typeof Switch & RNSwitch; + +export const TextInput = createNativeWrapper(RNTextInput); +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type TextInput = typeof TextInput & RNTextInput; + +export const DrawerLayoutAndroid = createNativeWrapper< + PropsWithChildren +>(RNDrawerLayoutAndroid, { disallowInterruption: true }); +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type DrawerLayoutAndroid = typeof DrawerLayoutAndroid & + RNDrawerLayoutAndroid; + +export const FlatList = ((props) => { + const refreshControlRef = + React.useRef>(null); + + const { requireExternalGestureToFail, refreshControl, ...rest } = props; + + const flatListProps = {}; + const scrollViewProps = {}; + for (const [propName, value] of Object.entries(rest)) { + // @ts-ignore https://github.com/microsoft/TypeScript/issues/26255 + if (NativeWrapperProps.has(propName)) { + // @ts-ignore - this function cannot have generic type so we have to ignore this error + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + scrollViewProps[propName] = value; + } else { + // @ts-ignore - this function cannot have generic type so we have to ignore this error + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + flatListProps[propName] = value; + } + } + + const waitFor: AnyGesture[] = []; + + if (Array.isArray(requireExternalGestureToFail)) { + waitFor.push(...requireExternalGestureToFail); + } else if (requireExternalGestureToFail) { + waitFor.push(requireExternalGestureToFail); + } + + if (refreshControlRef.current) { + waitFor.push(refreshControlRef.current.gestureRef); + } + + return ( + // @ts-ignore - this function cannot have generic type so we have to ignore this error + ( + + )} + // @ts-ignore we don't pass `refreshing` prop as we only want to override the ref + refreshControl={ + refreshControl + ? React.cloneElement(refreshControl, { + // @ts-ignore for reasons unknown to me, `ref` doesn't exist on the type inferred by TS + ref: refreshControlRef, + }) + : undefined + } + /> + ); +}) as ( + props: PropsWithChildren< + Omit, 'renderScrollComponent'> & + RefAttributes> & + NativeWrapperProperties + >, + ref?: ForwardedRef> +) => ReactElement | null; +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type FlatList = typeof FlatList & RNFlatList; From a0d0240cbb6ffee50f400b2ca7d56b400e718b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Tue, 4 Nov 2025 11:37:07 +0100 Subject: [PATCH 08/21] Deprecate legacy buttons --- .../src/components/GestureButtons.tsx | 40 ++++++++++++------- .../src/components/GestureButtonsProps.ts | 27 +++++++++---- .../src/components/GestureHandlerButton.tsx | 4 +- .../touchables/GenericTouchable.tsx | 6 +-- .../src/handlers/gestureHandlerTypesCompat.ts | 30 ++++++++++---- .../react-native-gesture-handler/src/index.ts | 26 ++++++------ 6 files changed, 86 insertions(+), 47 deletions(-) diff --git a/packages/react-native-gesture-handler/src/components/GestureButtons.tsx b/packages/react-native-gesture-handler/src/components/GestureButtons.tsx index 04409f7f2c..43ed5518a6 100644 --- a/packages/react-native-gesture-handler/src/components/GestureButtons.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureButtons.tsx @@ -12,14 +12,17 @@ import { import type { NativeViewGestureHandlerPayload } from '../handlers/GestureHandlerEventPayload'; import type { BaseButtonWithRefProps, - BaseButtonProps, + LegacyBaseButtonProps, RectButtonWithRefProps, - RectButtonProps, + LegacyRectButtonProps, BorderlessButtonWithRefProps, - BorderlessButtonProps, + LegacyBorderlessButtonProps, } from './GestureButtonsProps'; -export const RawButton = createNativeWrapper(GestureHandlerButton, { +/** + * @deprecated use `RawButton` instead + */ +export const LegacyRawButton = createNativeWrapper(GestureHandlerButton, { shouldCancelWhenOutside: false, shouldActivateOnStart: false, }); @@ -123,7 +126,7 @@ class InnerBaseButton extends React.Component { const { rippleColor, style, ...rest } = this.props; return ( - { const AnimatedInnerBaseButton = Animated.createAnimatedComponent(InnerBaseButton); -export const BaseButton = React.forwardRef< +/** + * @deprecated use `BaseButton` instead + */ +export const LegacyBaseButton = React.forwardRef< React.ComponentType, - Omit + Omit >((props, ref) => ); const AnimatedBaseButton = React.forwardRef< @@ -185,7 +191,7 @@ class InnerRectButton extends React.Component { const resolvedStyle = StyleSheet.flatten(style) ?? {}; return ( - { ]} /> {children} - + ); } } -export const RectButton = React.forwardRef< +/** + * @deprecated use `RectButton` instead + */ +export const LegacyRectButton = React.forwardRef< React.ComponentType, - Omit + Omit >((props, ref) => ); class InnerBorderlessButton extends React.Component { @@ -251,9 +260,12 @@ class InnerBorderlessButton extends React.Component + Omit >((props, ref) => ); -export { default as PureNativeButton } from './GestureHandlerButton'; +export { default as LegacyPureNativeButton } from './GestureHandlerButton'; diff --git a/packages/react-native-gesture-handler/src/components/GestureButtonsProps.ts b/packages/react-native-gesture-handler/src/components/GestureButtonsProps.ts index e9504bff32..fe4a97575f 100644 --- a/packages/react-native-gesture-handler/src/components/GestureButtonsProps.ts +++ b/packages/react-native-gesture-handler/src/components/GestureButtonsProps.ts @@ -8,7 +8,10 @@ import { } from 'react-native'; import type { NativeViewGestureHandlerProps } from '../handlers/NativeViewGestureHandler'; -export interface RawButtonProps +/** + * @deprecated use `RawButtonProps` with `RawButton` instead + */ +export interface LegacyRawButtonProps extends NativeViewGestureHandlerProps, AccessibilityProps { /** @@ -94,7 +97,10 @@ interface ButtonWithRefProps { innerRef?: React.ForwardedRef>; } -export interface BaseButtonProps extends RawButtonProps { +/** + * @deprecated use `BaseButtonProps` with `BaseButton` instead + */ +export interface LegacyBaseButtonProps extends LegacyRawButtonProps { /** * Called when the button gets pressed (analogous to `onPress` in * `TouchableHighlight` from RN core). @@ -123,10 +129,13 @@ export interface BaseButtonProps extends RawButtonProps { delayLongPress?: number; } export interface BaseButtonWithRefProps - extends BaseButtonProps, + extends LegacyBaseButtonProps, ButtonWithRefProps {} -export interface RectButtonProps extends BaseButtonProps { +/** + * @deprecated use `RectButtonProps` with `RectButton` instead + */ +export interface LegacyRectButtonProps extends LegacyBaseButtonProps { /** * Background color that will be dimmed when button is in active state. */ @@ -140,10 +149,14 @@ export interface RectButtonProps extends BaseButtonProps { activeOpacity?: number; } export interface RectButtonWithRefProps - extends RectButtonProps, + extends LegacyRectButtonProps, ButtonWithRefProps {} -export interface BorderlessButtonProps extends BaseButtonProps { +/** + * @deprecated use `BorderlessButtonProps` with `BorderlessButton` instead + */ + +export interface LegacyBorderlessButtonProps extends LegacyBaseButtonProps { /** * iOS only. * @@ -152,5 +165,5 @@ export interface BorderlessButtonProps extends BaseButtonProps { activeOpacity?: number; } export interface BorderlessButtonWithRefProps - extends BorderlessButtonProps, + extends LegacyBorderlessButtonProps, ButtonWithRefProps {} diff --git a/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx b/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx index b6f0c391c0..8f7ddcd1fb 100644 --- a/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx @@ -1,5 +1,5 @@ import { HostComponent } from 'react-native'; -import type { RawButtonProps } from './GestureButtonsProps'; +import type { LegacyRawButtonProps } from './GestureButtonsProps'; import RNGestureHandlerButtonNativeComponent from '../specs/RNGestureHandlerButtonNativeComponent'; -export default RNGestureHandlerButtonNativeComponent as HostComponent; +export default RNGestureHandlerButtonNativeComponent as HostComponent; diff --git a/packages/react-native-gesture-handler/src/components/touchables/GenericTouchable.tsx b/packages/react-native-gesture-handler/src/components/touchables/GenericTouchable.tsx index 6bbb344f49..01bff2671a 100644 --- a/packages/react-native-gesture-handler/src/components/touchables/GenericTouchable.tsx +++ b/packages/react-native-gesture-handler/src/components/touchables/GenericTouchable.tsx @@ -3,7 +3,7 @@ import { Component } from 'react'; import { Animated, Platform } from 'react-native'; import { State } from '../../State'; -import { BaseButton } from '../GestureButtons'; +import { LegacyBaseButton } from '../GestureButtons'; import { GestureEvent, @@ -251,7 +251,7 @@ export default class GenericTouchable extends Component< }; return ( - {this.props.children} - + ); } } diff --git a/packages/react-native-gesture-handler/src/handlers/gestureHandlerTypesCompat.ts b/packages/react-native-gesture-handler/src/handlers/gestureHandlerTypesCompat.ts index 050208ba2f..c4e4ff2506 100644 --- a/packages/react-native-gesture-handler/src/handlers/gestureHandlerTypesCompat.ts +++ b/packages/react-native-gesture-handler/src/handlers/gestureHandlerTypesCompat.ts @@ -1,8 +1,8 @@ import type { - BaseButtonProps, - BorderlessButtonProps, - RawButtonProps, - RectButtonProps, + LegacyBaseButtonProps, + LegacyBorderlessButtonProps, + LegacyRawButtonProps, + LegacyRectButtonProps, } from '../components/GestureButtonsProps'; import { GestureEvent, @@ -94,8 +94,22 @@ export type FlingGestureHandlerProperties = FlingGestureHandlerProps; * @deprecated ForceTouch gesture is deprecated and will be removed in the future. */ export type ForceTouchGestureHandlerProperties = ForceTouchGestureHandlerProps; + // Button props -export type RawButtonProperties = RawButtonProps; -export type BaseButtonProperties = BaseButtonProps; -export type RectButtonProperties = RectButtonProps; -export type BorderlessButtonProperties = BorderlessButtonProps; + +/** + * @deprecated Use RawButtonProperties instead + */ +export type LegacyRawButtonProperties = LegacyRawButtonProps; +/** + * @deprecated Use BaseButtonProperties instead + */ +export type LegacyBaseButtonProperties = LegacyBaseButtonProps; +/** + * @deprecated Use RectButtonProperties instead + */ +export type LegacyRectButtonProperties = LegacyRectButtonProps; +/** + * @deprecated Use BorderlessButtonProperties instead + */ +export type LegacyBorderlessButtonProperties = LegacyBorderlessButtonProps; diff --git a/packages/react-native-gesture-handler/src/index.ts b/packages/react-native-gesture-handler/src/index.ts index c508c06faa..24e7562a44 100644 --- a/packages/react-native-gesture-handler/src/index.ts +++ b/packages/react-native-gesture-handler/src/index.ts @@ -71,17 +71,17 @@ export type { export type { GestureStateManagerType as GestureStateManager } from './handlers/gestures/gestureStateManager'; export { NativeViewGestureHandler } from './handlers/NativeViewGestureHandler'; export type { - RawButtonProps, - BaseButtonProps, - RectButtonProps, - BorderlessButtonProps, + LegacyRawButtonProps, + LegacyBaseButtonProps, + LegacyRectButtonProps, + LegacyBorderlessButtonProps, } from './components/GestureButtonsProps'; export { - RawButton, - BaseButton, - RectButton, - BorderlessButton, - PureNativeButton, + LegacyRawButton, + LegacyBaseButton, + LegacyRectButton, + LegacyBorderlessButton, + LegacyPureNativeButton, } from './components/GestureButtons'; export type { TouchableHighlightProps, @@ -137,10 +137,10 @@ export type { FlingGestureHandlerProperties, ForceTouchGestureHandlerProperties, // Buttons props - RawButtonProperties, - BaseButtonProperties, - RectButtonProperties, - BorderlessButtonProperties, + LegacyRawButtonProperties, + LegacyBaseButtonProperties, + LegacyRectButtonProperties, + LegacyBorderlessButtonProperties, } from './handlers/gestureHandlerTypesCompat'; export type { From ef1e7363cacd19f64f9e15f123d7672f41f0fba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Tue, 4 Nov 2025 11:44:45 +0100 Subject: [PATCH 09/21] Deprecate legacy components --- .../src/components/GestureComponents.tsx | 58 +++++++++++++------ .../src/components/GestureComponents.web.tsx | 36 +++++++++--- .../react-native-gesture-handler/src/index.ts | 12 ++-- 3 files changed, 76 insertions(+), 30 deletions(-) diff --git a/packages/react-native-gesture-handler/src/components/GestureComponents.tsx b/packages/react-native-gesture-handler/src/components/GestureComponents.tsx index 8bdf73a35d..aac075a8d0 100644 --- a/packages/react-native-gesture-handler/src/components/GestureComponents.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureComponents.tsx @@ -28,12 +28,17 @@ import { import { toArray } from '../utils'; -export const RefreshControl = createNativeWrapper(RNRefreshControl, { +/** + * @deprecated use `RefreshControl` instead + */ +export const LegacyRefreshControl = createNativeWrapper(RNRefreshControl, { disallowInterruption: true, shouldCancelWhenOutside: false, }); + // eslint-disable-next-line @typescript-eslint/no-redeclare -export type RefreshControl = typeof RefreshControl & RNRefreshControl; +export type LegacyRefreshControl = typeof LegacyRefreshControl & + RNRefreshControl; const GHScrollView = createNativeWrapper>( RNScrollView, @@ -42,11 +47,15 @@ const GHScrollView = createNativeWrapper>( shouldCancelWhenOutside: false, } ); -export const ScrollView = React.forwardRef< + +/** + * @deprecated use `ScrollView` instead + */ +export const LegacyScrollView = React.forwardRef< RNScrollView, RNScrollViewProps & NativeViewGestureHandlerProps >((props, ref) => { - const refreshControlGestureRef = React.useRef(null); + const refreshControlGestureRef = React.useRef(null); const { refreshControl, waitFor, ...rest } = props; return ( @@ -67,32 +76,46 @@ export const ScrollView = React.forwardRef< /> ); }); + // Backward type compatibility with https://github.com/software-mansion/react-native-gesture-handler/blob/db78d3ca7d48e8ba57482d3fe9b0a15aa79d9932/react-native-gesture-handler.d.ts#L440-L457 // include methods of wrapped components by creating an intersection type with the RN component instead of duplicating them. // eslint-disable-next-line @typescript-eslint/no-redeclare -export type ScrollView = typeof GHScrollView & RNScrollView; +export type LegacyScrollView = typeof GHScrollView & RNScrollView; -export const Switch = createNativeWrapper(RNSwitch, { +/** + * @deprecated use `Switch` instead + */ +export const LegacySwitch = createNativeWrapper(RNSwitch, { shouldCancelWhenOutside: false, shouldActivateOnStart: true, disallowInterruption: true, }); // eslint-disable-next-line @typescript-eslint/no-redeclare -export type Switch = typeof Switch & RNSwitch; +export type LegacySwitch = typeof LegacySwitch & RNSwitch; -export const TextInput = createNativeWrapper(RNTextInput); +/** + * @deprecated use `RefreshControl` instead + */ +export const LegacyTextInput = + createNativeWrapper(RNTextInput); // eslint-disable-next-line @typescript-eslint/no-redeclare -export type TextInput = typeof TextInput & RNTextInput; +export type LegacyTextInput = typeof LegacyTextInput & RNTextInput; -export const DrawerLayoutAndroid = createNativeWrapper< +/** + * @deprecated use `DrawerLayoutAndroid` instead + */ +export const LegacyDrawerLayoutAndroid = createNativeWrapper< PropsWithChildren >(RNDrawerLayoutAndroid, { disallowInterruption: true }); // eslint-disable-next-line @typescript-eslint/no-redeclare -export type DrawerLayoutAndroid = typeof DrawerLayoutAndroid & +export type LegacyDrawerLayoutAndroid = typeof LegacyDrawerLayoutAndroid & RNDrawerLayoutAndroid; -export const FlatList = React.forwardRef((props, ref) => { - const refreshControlGestureRef = React.useRef(null); +/** + * @deprecated use `FlatList` instead + */ +export const LegacyFlatList = React.forwardRef((props, ref) => { + const refreshControlGestureRef = React.useRef(null); const { waitFor, refreshControl, ...rest } = props; @@ -117,7 +140,7 @@ export const FlatList = React.forwardRef((props, ref) => { ref={ref} {...flatListProps} renderScrollComponent={(scrollProps) => ( - { }) as ( props: PropsWithChildren< Omit, 'renderScrollComponent'> & - RefAttributes> & + RefAttributes> & NativeViewGestureHandlerProps >, - ref?: ForwardedRef> + ref?: ForwardedRef> ) => ReactElement | null; // eslint-disable-next-line @typescript-eslint/no-redeclare -export type FlatList = typeof FlatList & RNFlatList; +export type LegacyFlatList = typeof LegacyFlatList & + RNFlatList; diff --git a/packages/react-native-gesture-handler/src/components/GestureComponents.web.tsx b/packages/react-native-gesture-handler/src/components/GestureComponents.web.tsx index ccc03eaf8d..6526532342 100644 --- a/packages/react-native-gesture-handler/src/components/GestureComponents.web.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureComponents.web.tsx @@ -10,32 +10,54 @@ import { import createNativeWrapper from '../handlers/createNativeWrapper'; -export const ScrollView = createNativeWrapper(RNScrollView, { +/** + * @deprecated use `ScrollView` instead + */ +export const LegacyScrollView = createNativeWrapper(RNScrollView, { disallowInterruption: false, }); -export const Switch = createNativeWrapper(RNSwitch, { +/** + * @deprecated use `Switch` instead + */ +export const LegacySwitch = createNativeWrapper(RNSwitch, { shouldCancelWhenOutside: false, shouldActivateOnStart: true, disallowInterruption: true, }); -export const TextInput = createNativeWrapper(RNTextInput); -export const DrawerLayoutAndroid = () => { + +/** + * @deprecated use `TextInput` instead + */ +export const LegacyTextInput = createNativeWrapper(RNTextInput); + +/** + * @deprecated use `DrawerLayoutAndroid` instead + */ +export const LegacyDrawerLayoutAndroid = () => { console.warn('DrawerLayoutAndroid is not supported on web!'); return ; }; +/** + * @deprecated use `RefreshControl` instead + */ // RefreshControl is implemented as a functional component, rendering a View // NativeViewGestureHandler needs to set a ref on its child, which cannot be done // on functional components -export const RefreshControl = createNativeWrapper(View); +export const LegacyRefreshControl = createNativeWrapper(View); -export const FlatList = React.forwardRef( +/** + * @deprecated use `FlatList` instead + */ +export const LegacyFlatList = React.forwardRef( (props: FlatListProps, ref: any) => ( } + renderScrollComponent={(scrollProps) => ( + + )} /> ) ); diff --git a/packages/react-native-gesture-handler/src/index.ts b/packages/react-native-gesture-handler/src/index.ts index 24e7562a44..da08def4a6 100644 --- a/packages/react-native-gesture-handler/src/index.ts +++ b/packages/react-native-gesture-handler/src/index.ts @@ -95,12 +95,12 @@ export { TouchableWithoutFeedback, } from './components/touchables'; export { - ScrollView, - Switch, - TextInput, - DrawerLayoutAndroid, - FlatList, - RefreshControl, + LegacyScrollView, + LegacySwitch, + LegacyTextInput, + LegacyDrawerLayoutAndroid, + LegacyFlatList, + LegacyRefreshControl, } from './components/GestureComponents'; export { Text } from './components/Text'; export { HoverEffect } from './handlers/gestures/hoverGesture'; From 563fab35424a97ab0811c00ac776eb83f187c5b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Tue, 4 Nov 2025 11:48:57 +0100 Subject: [PATCH 10/21] Export new components --- .../react-native-gesture-handler/src/index.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/react-native-gesture-handler/src/index.ts b/packages/react-native-gesture-handler/src/index.ts index da08def4a6..7ac68a9285 100644 --- a/packages/react-native-gesture-handler/src/index.ts +++ b/packages/react-native-gesture-handler/src/index.ts @@ -76,6 +76,12 @@ export type { LegacyRectButtonProps, LegacyBorderlessButtonProps, } from './components/GestureButtonsProps'; +export type { + RawButtonProps, + BaseButtonProps, + RectButtonProps, + BorderlessButtonProps, +} from './v3/components/GestureButtonsProps'; export { LegacyRawButton, LegacyBaseButton, @@ -83,6 +89,13 @@ export { LegacyBorderlessButton, LegacyPureNativeButton, } from './components/GestureButtons'; +export { + RawButton, + BaseButton, + RectButton, + BorderlessButton, + PureNativeButton, +} from './v3/components/GestureButtons'; export type { TouchableHighlightProps, TouchableOpacityProps, @@ -102,6 +115,14 @@ export { LegacyFlatList, LegacyRefreshControl, } from './components/GestureComponents'; +export { + ScrollView, + Switch, + TextInput, + DrawerLayoutAndroid, + FlatList, + RefreshControl, +} from './v3/components/GestureComponents'; export { Text } from './components/Text'; export { HoverEffect } from './handlers/gestures/hoverGesture'; export type { From 0f40c583c1bff7217cc3f9209a7f7d86d3c042c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Tue, 4 Nov 2025 11:51:21 +0100 Subject: [PATCH 11/21] Add web file --- .../v3/components/GestureComponents.web.tsx | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 packages/react-native-gesture-handler/src/v3/components/GestureComponents.web.tsx diff --git a/packages/react-native-gesture-handler/src/v3/components/GestureComponents.web.tsx b/packages/react-native-gesture-handler/src/v3/components/GestureComponents.web.tsx new file mode 100644 index 0000000000..a9e69cc62d --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/components/GestureComponents.web.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import { + FlatList as RNFlatList, + Switch as RNSwitch, + TextInput as RNTextInput, + ScrollView as RNScrollView, + FlatListProps, + View, +} from 'react-native'; + +import createNativeWrapper from '../createNativeWrapper'; + +export const ScrollView = createNativeWrapper(RNScrollView, { + disallowInterruption: false, +}); + +export const Switch = createNativeWrapper(RNSwitch, { + shouldCancelWhenOutside: false, + shouldActivateOnStart: true, + disallowInterruption: true, +}); + +export const TextInput = createNativeWrapper(RNTextInput); + +export const DrawerLayoutAndroid = () => { + console.warn('DrawerLayoutAndroid is not supported on web!'); + return ; +}; + +// RefreshControl is implemented as a functional component, rendering a View +// NativeViewGestureHandler needs to set a ref on its child, which cannot be done +// on functional components +export const RefreshControl = createNativeWrapper(View); + +export const FlatList = React.forwardRef( + (props: FlatListProps, ref: any) => ( + } + /> + ) +); From b77cc2b0dca4009a9f95210ce527f6e96df2509a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Tue, 4 Nov 2025 15:42:19 +0100 Subject: [PATCH 12/21] Update import --- .../react-native-gesture-handler/src/v3/createNativeWrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx b/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx index 048ffa89a6..01be5c8bf5 100644 --- a/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx +++ b/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { NativeWrapperProps } from './hooks/utils'; import { useNative } from './hooks/gestures'; -import { NativeDetector } from './NativeDetector/NativeDetector'; +import { NativeDetector } from './detectors/NativeDetector'; import type { NativeWrapperProperties } from './types/NativeWrapperType'; import { NativeViewGestureConfig, From c3364538e02a45fb34ac5426a54730f30f093f20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Tue, 4 Nov 2025 17:48:14 +0100 Subject: [PATCH 13/21] use ref in check --- .../src/v3/components/GestureComponents.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/components/GestureComponents.tsx b/packages/react-native-gesture-handler/src/v3/components/GestureComponents.tsx index ad7a33358b..ed7a6b2738 100644 --- a/packages/react-native-gesture-handler/src/v3/components/GestureComponents.tsx +++ b/packages/react-native-gesture-handler/src/v3/components/GestureComponents.tsx @@ -57,7 +57,7 @@ export const ScrollView = ( waitFor.push(requireExternalGestureToFail); } - if (refreshControlRef.current) { + if (refreshControlRef.current?.gestureRef) { waitFor.push(refreshControlRef.current.gestureRef); } @@ -132,7 +132,7 @@ export const FlatList = ((props) => { waitFor.push(requireExternalGestureToFail); } - if (refreshControlRef.current) { + if (refreshControlRef.current?.gestureRef) { waitFor.push(refreshControlRef.current.gestureRef); } From c3f4320344a64cc680d76a26c4d4e8881785ba3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Tue, 4 Nov 2025 18:08:54 +0100 Subject: [PATCH 14/21] Correct refs --- .../src/v3/createNativeWrapper.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx b/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx index 01be5c8bf5..68c492ef72 100644 --- a/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx +++ b/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx @@ -51,8 +51,8 @@ export default function createNativeWrapper

( const gestureRef = React.useRef(native); React.useImperativeHandle(props.ref, () => ({ - componentRef, - gestureRef, + componentRef: componentRef.current, + gestureRef: gestureRef.current, })); return ( From c8f7c8c4ee81c4a4e9aef606557af41e9e35e54c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Fri, 14 Nov 2025 18:25:31 +0100 Subject: [PATCH 15/21] Add examples --- apps/common-app/App.tsx | 12 ++++ .../src/components/buttons/index.tsx | 58 +++++++++++++++++++ .../src/components/flatlist/index.tsx | 44 ++++++++++++++ .../src/components/scrollview/index.tsx | 36 ++++++++++++ 4 files changed, 150 insertions(+) create mode 100644 apps/common-app/src/components/buttons/index.tsx create mode 100644 apps/common-app/src/components/flatlist/index.tsx create mode 100644 apps/common-app/src/components/scrollview/index.tsx diff --git a/apps/common-app/App.tsx b/apps/common-app/App.tsx index 2e4a95d279..e2a9cff07a 100644 --- a/apps/common-app/App.tsx +++ b/apps/common-app/App.tsx @@ -76,6 +76,10 @@ import LongPressExample from './src/simple/longPress'; import ManualExample from './src/simple/manual'; import SimpleFling from './src/simple/fling'; +import FlatListExample from './src/components/flatlist'; +import ScrollViewExample from './src/components/scrollview'; +import ButtonsExample from './src/components/buttons'; + import { Icon } from '@swmansion/icons'; interface Example { @@ -114,6 +118,14 @@ const EXAMPLES: ExamplesSection[] = [ }, ], }, + { + sectionTitle: 'Components', + data: [ + { name: 'FlatList example', component: FlatListExample }, + { name: 'ScrollView example', component: ScrollViewExample }, + { name: 'Buttons example', component: ButtonsExample }, + ], + }, { sectionTitle: 'Basic examples', data: [ diff --git a/apps/common-app/src/components/buttons/index.tsx b/apps/common-app/src/components/buttons/index.tsx new file mode 100644 index 0000000000..014aa4e2d7 --- /dev/null +++ b/apps/common-app/src/components/buttons/index.tsx @@ -0,0 +1,58 @@ +import { StyleSheet, Text } from 'react-native'; +import { + BaseButton, + BorderlessButton, + GestureHandlerRootView, + RectButton, +} from 'react-native-gesture-handler'; + +type ButtonWrapperProps = { + ButtonComponent: + | typeof BaseButton + | typeof RectButton + | typeof BorderlessButton; + + color: string; +}; + +function ButtonWrapper({ ButtonComponent, color }: ButtonWrapperProps) { + return ( + console.log(`[${ButtonComponent.name}] Hello World!`)}> + {ButtonComponent.name} + + ); +} + +export default function ButtonsExample() { + return ( + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'space-around', + }, + button: { + width: 200, + height: 50, + borderRadius: 15, + + display: 'flex', + alignItems: 'center', + justifyContent: 'space-around', + }, + + buttonText: { + color: 'white', + fontSize: 16, + }, +}); diff --git a/apps/common-app/src/components/flatlist/index.tsx b/apps/common-app/src/components/flatlist/index.tsx new file mode 100644 index 0000000000..19482e5ea2 --- /dev/null +++ b/apps/common-app/src/components/flatlist/index.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { FlatList, GestureHandlerRootView } from 'react-native-gesture-handler'; + +const DATA = Array.from({ length: 20 }, (_, i) => ({ + id: i.toString(), + title: `Item ${i + 1}`, +})); + +const Item = ({ title }: { title: string }) => ( + + {title} + +); + +export default function FlatListExample() { + return ( + + item.id} + renderItem={({ item }) => } + contentContainerStyle={styles.container} + /> + ; + + ); +} + +const styles = StyleSheet.create({ + container: { + paddingVertical: 16, + }, + item: { + backgroundColor: '#f9c2ff', + padding: 20, + marginVertical: 8, + marginHorizontal: 16, + borderRadius: 8, + }, + title: { + fontSize: 18, + }, +}); diff --git a/apps/common-app/src/components/scrollview/index.tsx b/apps/common-app/src/components/scrollview/index.tsx new file mode 100644 index 0000000000..72f5923723 --- /dev/null +++ b/apps/common-app/src/components/scrollview/index.tsx @@ -0,0 +1,36 @@ +import React, { useState } from 'react'; +import { Text, StyleSheet } from 'react-native'; +import { ScrollView, RefreshControl } from 'react-native-gesture-handler'; + +export default function ScrollViewExample() { + const [refreshing, setRefreshing] = useState(false); + + const onRefresh = () => { + setRefreshing(true); + setTimeout(() => { + setRefreshing(false); + }, 1500); + }; + + return ( + + }> + Pull down to refresh! + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + }, + text: { + fontSize: 18, + }, +}); From 2a356f8630c4c8ce3da2989084aa502a90ebcba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Fri, 14 Nov 2025 19:41:50 +0100 Subject: [PATCH 16/21] Fix buttons --- .../src/components/buttons/index.tsx | 5 +- .../src/v3/components/GestureButtons.tsx | 63 +++++++++---------- 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/apps/common-app/src/components/buttons/index.tsx b/apps/common-app/src/components/buttons/index.tsx index 014aa4e2d7..d64cfc4cab 100644 --- a/apps/common-app/src/components/buttons/index.tsx +++ b/apps/common-app/src/components/buttons/index.tsx @@ -19,7 +19,10 @@ function ButtonWrapper({ ButtonComponent, color }: ButtonWrapperProps) { return ( console.log(`[${ButtonComponent.name}] Hello World!`)}> + onPress={() => console.log(`[${ButtonComponent.name}] onPress`)} + onLongPress={() => { + console.log(`[${ButtonComponent.name}] onLongPress`); + }}> {ButtonComponent.name} ); diff --git a/packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx b/packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx index 8821af2e55..45a27c9402 100644 --- a/packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx +++ b/packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useRef } from 'react'; import { Platform, StyleSheet, Animated } from 'react-native'; import createNativeWrapper from '../createNativeWrapper'; import GestureHandlerButton from '../../components/GestureHandlerButton'; @@ -19,9 +19,10 @@ export const RawButton = createNativeWrapper(GestureHandlerButton, { }); export const BaseButton = (props: BaseButtonProps) => { - let lastActive: boolean; - let longPressDetected: boolean; - let longPressTimeout: ReturnType | undefined; + const longPressDetected = useRef(false); + const longPressTimeout = useRef | undefined>( + undefined + ); const delayLongPress = props.delayLongPress ?? 600; @@ -35,37 +36,36 @@ export const BaseButton = (props: BaseButtonProps) => { } = props; const wrappedLongPress = () => { - longPressDetected = true; + longPressDetected.current = true; onLongPress?.(); }; const onBegin = (e: CallbackEventType) => { if (Platform.OS === 'android' && e.handlerData.pointerInside) { - longPressDetected = false; + longPressDetected.current = false; if (onLongPress) { - longPressTimeout = setTimeout(wrappedLongPress, delayLongPress); + longPressTimeout.current = setTimeout(wrappedLongPress, delayLongPress); } } - - lastActive = false; }; const onStart = (e: CallbackEventType) => { onActiveStateChange?.(true); if (Platform.OS !== 'android' && e.handlerData.pointerInside) { - longPressDetected = false; + longPressDetected.current = false; if (onLongPress) { - longPressTimeout = setTimeout(wrappedLongPress, delayLongPress); + longPressTimeout.current = setTimeout(wrappedLongPress, delayLongPress); } } - if (!e.handlerData.pointerInside && longPressTimeout !== undefined) { - clearTimeout(longPressTimeout); - longPressTimeout = undefined; + if ( + !e.handlerData.pointerInside && + longPressTimeout.current !== undefined + ) { + clearTimeout(longPressTimeout.current); + longPressTimeout.current = undefined; } - - lastActive = true; }; const onEnd = (e: CallbackEventType, success: boolean) => { @@ -73,25 +73,18 @@ export const BaseButton = (props: BaseButtonProps) => { return; } - if (!longPressDetected && onPress) { + if (!longPressDetected.current && onPress) { onPress(e.handlerData.pointerInside); } - if (lastActive) { - onActiveStateChange?.(false); - } + onActiveStateChange?.(false); }; const onFinalize = (_e: CallbackEventType) => { - if (lastActive) { - onActiveStateChange?.(false); - } - - if (longPressTimeout !== undefined) { - clearTimeout(longPressTimeout); - longPressTimeout = undefined; + if (longPressTimeout.current !== undefined) { + clearTimeout(longPressTimeout.current); + longPressTimeout.current = undefined; } - lastActive = false; }; return ( @@ -108,6 +101,8 @@ export const BaseButton = (props: BaseButtonProps) => { ); }; +const AnimatedBaseButton = Animated.createAnimatedComponent(BaseButton); + const btnStyles = StyleSheet.create({ underlay: { position: 'absolute', @@ -122,7 +117,7 @@ export const RectButton = (props: RectButtonProps) => { const activeOpacity = props.activeOpacity ?? 0.105; const underlayColor = props.underlayColor ?? 'black'; - const opacity = new Animated.Value(0); + const opacity = useRef(new Animated.Value(0)).current; const onActiveStateChange = (active: boolean) => { if (Platform.OS !== 'android') { @@ -162,11 +157,11 @@ export const RectButton = (props: RectButtonProps) => { export const BorderlessButton = (props: BorderlessButtonProps) => { const activeOpacity = props.activeOpacity ?? 0.3; - const opacity = new Animated.Value(1); + const opacity = useRef(new Animated.Value(1)).current; const onActiveStateChange = (active: boolean) => { - if (Platform.OS !== 'android') { - opacity.setValue(active ? activeOpacity : 0); + if (Platform.OS === 'ios') { + opacity.setValue(active ? activeOpacity : 1); } props.onActiveStateChange?.(active); @@ -175,12 +170,12 @@ export const BorderlessButton = (props: BorderlessButtonProps) => { const { children, style, ...rest } = props; return ( - {children} - + ); }; From 73969dac643d70310686c439662f402c4e9b6ed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Fri, 14 Nov 2025 19:47:17 +0100 Subject: [PATCH 17/21] Yet another fix --- .../src/v3/components/GestureButtons.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx b/packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx index 45a27c9402..f91e8930cb 100644 --- a/packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx +++ b/packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx @@ -69,15 +69,11 @@ export const BaseButton = (props: BaseButtonProps) => { }; const onEnd = (e: CallbackEventType, success: boolean) => { - if (!success) { - return; - } + onActiveStateChange?.(false); - if (!longPressDetected.current && onPress) { - onPress(e.handlerData.pointerInside); + if (success && !longPressDetected.current) { + onPress?.(e.handlerData.pointerInside); } - - onActiveStateChange?.(false); }; const onFinalize = (_e: CallbackEventType) => { From f4fa5f66d99e960613dafe485e48d5fb634bb41d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Fri, 14 Nov 2025 19:54:19 +0100 Subject: [PATCH 18/21] Remove Drawer Layout --- .../src/v3/components/GestureComponents.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/components/GestureComponents.tsx b/packages/react-native-gesture-handler/src/v3/components/GestureComponents.tsx index 18d18bd115..2623e1ce2a 100644 --- a/packages/react-native-gesture-handler/src/v3/components/GestureComponents.tsx +++ b/packages/react-native-gesture-handler/src/v3/components/GestureComponents.tsx @@ -12,8 +12,6 @@ import { SwitchProps as RNSwitchProps, TextInput as RNTextInput, TextInputProps as RNTextInputProps, - DrawerLayoutAndroid as RNDrawerLayoutAndroid, - DrawerLayoutAndroidProps as RNDrawerLayoutAndroidProps, FlatList as RNFlatList, FlatListProps as RNFlatListProps, RefreshControl as RNRefreshControl, @@ -96,13 +94,6 @@ export const TextInput = createNativeWrapper(RNTextInput); // eslint-disable-next-line @typescript-eslint/no-redeclare export type TextInput = typeof TextInput & RNTextInput; -export const DrawerLayoutAndroid = createNativeWrapper< - PropsWithChildren ->(RNDrawerLayoutAndroid, { disallowInterruption: true }); -// eslint-disable-next-line @typescript-eslint/no-redeclare -export type DrawerLayoutAndroid = typeof DrawerLayoutAndroid & - RNDrawerLayoutAndroid; - export const FlatList = ((props) => { const refreshControlRef = React.useRef>(null); From ede51fe9dd93ed69385ee807d7880082c948b9d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Sat, 15 Nov 2025 11:27:12 +0100 Subject: [PATCH 19/21] Impoort hooks directly --- .../src/v3/createNativeWrapper.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx b/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx index 68c492ef72..05f43c3ba5 100644 --- a/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx +++ b/packages/react-native-gesture-handler/src/v3/createNativeWrapper.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { useImperativeHandle, useRef } from 'react'; import { NativeWrapperProps } from './hooks/utils'; import { useNative } from './hooks/gestures'; @@ -47,10 +47,10 @@ export default function createNativeWrapper

( const native = useNative(gestureHandlerProps); - const componentRef = React.useRef>(null); - const gestureRef = React.useRef(native); + const componentRef = useRef>(null); + const gestureRef = useRef(native); - React.useImperativeHandle(props.ref, () => ({ + useImperativeHandle(props.ref, () => ({ componentRef: componentRef.current, gestureRef: gestureRef.current, })); From d34ef86ccb4c1992b471da5eb28eb10d83fdd0f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Sat, 15 Nov 2025 11:30:45 +0100 Subject: [PATCH 20/21] remove import --- packages/react-native-gesture-handler/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-native-gesture-handler/src/index.ts b/packages/react-native-gesture-handler/src/index.ts index 4fc7e5abf9..6e189eb528 100644 --- a/packages/react-native-gesture-handler/src/index.ts +++ b/packages/react-native-gesture-handler/src/index.ts @@ -119,7 +119,6 @@ export { ScrollView, Switch, TextInput, - DrawerLayoutAndroid, FlatList, RefreshControl, } from './v3/components/GestureComponents'; From fdf9533b7b927f380388aee64ef262116af53e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Sat, 15 Nov 2025 11:31:18 +0100 Subject: [PATCH 21/21] Update examples --- .../src/basic/pagerAndDrawer/index.android.tsx | 10 +++++----- apps/common-app/src/new_api/calculator/index.tsx | 10 +++++----- apps/common-app/src/recipes/panAndScroll/index.tsx | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/common-app/src/basic/pagerAndDrawer/index.android.tsx b/apps/common-app/src/basic/pagerAndDrawer/index.android.tsx index d7a467f834..39fa22d0e5 100644 --- a/apps/common-app/src/basic/pagerAndDrawer/index.android.tsx +++ b/apps/common-app/src/basic/pagerAndDrawer/index.android.tsx @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { createNativeWrapper, - DrawerLayoutAndroid, + LegacyDrawerLayoutAndroid, } from 'react-native-gesture-handler'; const WrappedViewPagerAndroid = createNativeWrapper(ViewPagerAndroid, { @@ -35,22 +35,22 @@ export default class Example extends Component { return ( - navigationView}> - + - navigationView}> - + ); diff --git a/apps/common-app/src/new_api/calculator/index.tsx b/apps/common-app/src/new_api/calculator/index.tsx index 84b48fcb4f..07c92a6282 100644 --- a/apps/common-app/src/new_api/calculator/index.tsx +++ b/apps/common-app/src/new_api/calculator/index.tsx @@ -10,7 +10,7 @@ import { import { GestureDetector, Gesture, - ScrollView, + LegacyScrollView, } from 'react-native-gesture-handler'; import Animated, { useSharedValue, @@ -60,7 +60,7 @@ interface OutputProps { function Output({ offset, expression, history }: OutputProps) { const layout = useRef({}); - const scrollView = useRef(null); + const scrollView = useRef(null); const drag = useSharedValue(0); const dragOffset = useSharedValue(0); const [opened, setOpened] = useState(false); @@ -130,8 +130,8 @@ function Output({ offset, expression, history }: OutputProps) { - { + { if (!opened) { ref?.scrollToEnd({ animated: false }); } @@ -146,7 +146,7 @@ function Output({ offset, expression, history }: OutputProps) { })} - + diff --git a/apps/common-app/src/recipes/panAndScroll/index.tsx b/apps/common-app/src/recipes/panAndScroll/index.tsx index 87dbe28caa..b31f52faeb 100644 --- a/apps/common-app/src/recipes/panAndScroll/index.tsx +++ b/apps/common-app/src/recipes/panAndScroll/index.tsx @@ -3,10 +3,10 @@ import { Animated, Dimensions, StyleSheet } from 'react-native'; import { PanGestureHandler, TapGestureHandler, - ScrollView, State, PanGestureHandlerGestureEvent, TapGestureHandlerStateChangeEvent, + LegacyScrollView, } from 'react-native-gesture-handler'; import { USE_NATIVE_DRIVER } from '../../config'; @@ -92,11 +92,11 @@ export default class Example extends Component { const tapRef = React.createRef(); const panRef = React.createRef(); return ( - + - + ); } }