diff --git a/.gitignore b/.gitignore
index 8f4dff35ec..c6c013e0ae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,7 @@ DerivedData
# Android/IntelliJ
#
+bin/
build/
.idea
.gradle
diff --git a/apps/common-app/src/common.tsx b/apps/common-app/src/common.tsx
index b8c35a81c4..0196f52109 100644
--- a/apps/common-app/src/common.tsx
+++ b/apps/common-app/src/common.tsx
@@ -36,15 +36,21 @@ export const COLORS = {
offWhite: '#f8f9ff',
headerSeparator: '#eef0ff',
PURPLE: '#b58df1',
+ DARK_PURPLE: '#7d63d9',
NAVY: '#001A72',
RED: '#A41623',
YELLOW: '#F2AF29',
GREEN: '#0F956F',
+ DARK_GREEN: '#217838',
GRAY: '#ADB1C2',
KINDA_RED: '#FFB2AD',
+ DARK_SALMON: '#d97973',
KINDA_YELLOW: '#FFF096',
KINDA_GREEN: '#C4E7DB',
KINDA_BLUE: '#A0D5EF',
+ LIGHT_BLUE: '#5f97c8',
+ WEB_BLUE: '#1067c4',
+ ANDROID: '#34a853',
};
/* eslint-disable react-native/no-unused-styles */
diff --git a/apps/common-app/src/new_api/components/clickable/index.tsx b/apps/common-app/src/new_api/components/clickable/index.tsx
new file mode 100644
index 0000000000..4b52cde8f6
--- /dev/null
+++ b/apps/common-app/src/new_api/components/clickable/index.tsx
@@ -0,0 +1,158 @@
+import React from 'react';
+import { StyleSheet, Text, View, ScrollView } from 'react-native';
+import {
+ GestureHandlerRootView,
+ Clickable,
+ ClickableProps,
+} from 'react-native-gesture-handler';
+import { COLORS } from '../../../common';
+
+type ButtonWrapperProps = ClickableProps & {
+ name: string;
+ color: string;
+};
+
+function ClickableWrapper({ name, color, ...rest }: ButtonWrapperProps) {
+ return (
+ console.log(`[${name}] onPressIn`)}
+ onPress={() => console.log(`[${name}] onPress`)}
+ onLongPress={() => console.log(`[${name}] onLongPress`)}
+ onPressOut={() => console.log(`[${name}] onPressOut`)}
+ {...rest}>
+ {name}
+
+ );
+}
+
+export default function ClickableExample() {
+ return (
+
+
+
+ Buttons replacements
+ New component that replaces all buttons and pressables.
+
+
+
+
+
+
+
+
+
+
+
+ Custom animations
+ Animated overlay.
+
+
+
+
+
+
+
+ Animated component.
+
+
+
+
+
+
+
+
+
+ Android ripple
+ Configurable ripple effect on Clickable component.
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ scrollContent: {
+ paddingBottom: 40,
+ },
+ section: {
+ padding: 20,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: '#ccc',
+ alignItems: 'center',
+ },
+ row: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ gap: 10,
+ marginTop: 20,
+ marginBottom: 20,
+ },
+ sectionHeader: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ marginBottom: 4,
+ },
+ button: {
+ width: 110,
+ height: 50,
+ borderRadius: 12,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ buttonText: {
+ color: 'white',
+ fontSize: 14,
+ fontWeight: '600',
+ },
+});
diff --git a/apps/common-app/src/new_api/components/clickable_stress/index.tsx b/apps/common-app/src/new_api/components/clickable_stress/index.tsx
new file mode 100644
index 0000000000..47d876ade1
--- /dev/null
+++ b/apps/common-app/src/new_api/components/clickable_stress/index.tsx
@@ -0,0 +1,193 @@
+import { Profiler, useCallback, useEffect, useRef, useState } from 'react';
+import { StyleSheet, Text, View } from 'react-native';
+import { Clickable, ScrollView } from 'react-native-gesture-handler';
+
+const CLICK_COUNT = 2000;
+const N = 25;
+const DROPOUT = 3;
+
+const STRESS_DATA = Array.from(
+ { length: CLICK_COUNT },
+ (_, i) => `stress-${i}`
+);
+
+type BenchmarkState =
+ | { phase: 'idle' }
+ | { phase: 'running'; run: number }
+ | { phase: 'done'; results: number[] };
+
+function getTrimmedAverage(results: number[], dropout: number): number {
+ const sorted = [...results].sort((a, b) => a - b);
+ const trimCount = Math.min(
+ dropout,
+ Math.max(0, Math.floor((sorted.length - 1) / 2))
+ );
+ const trimmed =
+ trimCount > 0 ? sorted.slice(trimCount, sorted.length - trimCount) : sorted;
+ return trimmed.reduce((sum, v) => sum + v, 0) / trimmed.length;
+}
+
+type ClickableListProps = {
+ run: number;
+ onMountDuration: (duration: number) => void;
+};
+
+function ClickableList({ run, onMountDuration }: ClickableListProps) {
+ const reportedRef = useRef(-1);
+
+ const handleRender = useCallback(
+ (_id: string, phase: string, actualDuration: number) => {
+ if (phase === 'mount' && reportedRef.current !== run) {
+ reportedRef.current = run;
+ onMountDuration(actualDuration);
+ }
+ },
+ [run, onMountDuration]
+ );
+
+ return (
+
+
+ {STRESS_DATA.map((id) => (
+ //
+
+
+ //
+ //
+
+ //
+ //
+ ))}
+
+
+ );
+}
+
+export default function ClickableStress() {
+ const [state, setState] = useState({ phase: 'idle' });
+ const resultsRef = useRef([]);
+ const timeoutRef = useRef | null>(null);
+
+ const start = useCallback(() => {
+ resultsRef.current = [];
+ setState({ phase: 'running', run: 1 });
+ }, []);
+
+ const handleMountDuration = useCallback((duration: number) => {
+ resultsRef.current = [...resultsRef.current, duration];
+ const currentRun = resultsRef.current.length;
+
+ if (currentRun >= N) {
+ setState({ phase: 'done', results: resultsRef.current });
+ return;
+ }
+
+ // Unmount then remount for next run
+ setState({ phase: 'idle' });
+ if (timeoutRef.current !== null) {
+ clearTimeout(timeoutRef.current);
+ }
+ timeoutRef.current = setTimeout(() => {
+ setState({ phase: 'running', run: currentRun + 1 });
+ }, 50);
+ }, []);
+
+ useEffect(() => {
+ return () => {
+ if (timeoutRef.current !== null) {
+ clearTimeout(timeoutRef.current);
+ timeoutRef.current = null;
+ }
+ };
+ }, []);
+
+ const isRunning = state.phase === 'running';
+ const currentRun = state.phase === 'running' ? state.run : 0;
+ const results = state.phase === 'done' ? state.results : null;
+ const trimmedAverage = results ? getTrimmedAverage(results, DROPOUT) : null;
+
+ return (
+
+
+
+ {isRunning ? `Running ${currentRun}/${N}...` : 'Start test'}
+
+
+
+ {results && (
+
+
+ Runs: {results.length} (trimmed ±{DROPOUT})
+
+
+ Trimmed avg: {trimmedAverage?.toFixed(2)} ms
+
+
+ Min: {Math.min(...results).toFixed(2)} ms
+
+
+ Max: {Math.max(...results).toFixed(2)} ms
+
+
+ All: {results.map((r) => r.toFixed(1)).join(', ')} ms
+
+
+ )}
+
+ {isRunning && (
+
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 20,
+ alignItems: 'center',
+ },
+ startButton: {
+ width: 200,
+ height: 50,
+ backgroundColor: '#167a5f',
+ borderRadius: 10,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ startButtonBusy: {
+ backgroundColor: '#7f879b',
+ },
+ startButtonText: {
+ color: 'white',
+ fontWeight: '700',
+ },
+ button: {
+ width: 200,
+ height: 50,
+ backgroundColor: 'lightblue',
+ borderRadius: 10,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ results: {
+ marginTop: 20,
+ padding: 16,
+ borderRadius: 12,
+ backgroundColor: '#eef3fb',
+ width: '100%',
+ gap: 6,
+ },
+ resultText: {
+ color: '#33415c',
+ fontSize: 13,
+ },
+});
diff --git a/apps/common-app/src/new_api/index.tsx b/apps/common-app/src/new_api/index.tsx
index 3c706cf91b..602cb6ceab 100644
--- a/apps/common-app/src/new_api/index.tsx
+++ b/apps/common-app/src/new_api/index.tsx
@@ -27,6 +27,8 @@ import RotationExample from './simple/rotation';
import TapExample from './simple/tap';
import ButtonsExample from './components/buttons';
+import ClickableExample from './components/clickable';
+import ClickableStressExample from './components/clickable_stress';
import ReanimatedDrawerLayout from './components/drawer';
import FlatListExample from './components/flatlist';
import ScrollViewExample from './components/scrollview';
@@ -105,6 +107,8 @@ export const NEW_EXAMPLES: ExamplesSection[] = [
{ name: 'FlatList example', component: FlatListExample },
{ name: 'ScrollView example', component: ScrollViewExample },
{ name: 'Buttons example', component: ButtonsExample },
+ { name: 'Clickable example', component: ClickableExample },
+ { name: 'Clickable stress test', component: ClickableStressExample },
{ name: 'Switch & TextInput', component: SwitchTextInputExample },
{ name: 'Reanimated Swipeable', component: Swipeable },
{ name: 'Reanimated Drawer Layout', component: ReanimatedDrawerLayout },
diff --git a/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx b/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx
index f3f8758f0c..c99a8b7323 100644
--- a/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx
+++ b/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx
@@ -3,8 +3,9 @@ import { render, renderHook } from '@testing-library/react-native';
import { fireGestureHandler, getByGestureTestId } from '../jestUtils';
import { State } from '../State';
import GestureHandlerRootView from '../components/GestureHandlerRootView';
-import { RectButton } from '../v3/components';
+import { RectButton, Clickable } from '../v3/components';
import { act } from 'react';
+import type { SingleGesture } from '../v3/types';
describe('[API v3] Hooks', () => {
test('Pan gesture', () => {
@@ -57,4 +58,152 @@ describe('[API v3] Components', () => {
expect(pressFn).toHaveBeenCalledTimes(1);
});
+
+ describe('Clickable', () => {
+ test('calls onPress on successful press', () => {
+ const pressFn = jest.fn();
+
+ const Example = () => (
+
+
+
+ );
+
+ render();
+ const gesture = getByGestureTestId('clickable');
+
+ act(() => {
+ fireGestureHandler(gesture, [
+ { oldState: State.UNDETERMINED, state: State.BEGAN },
+ { oldState: State.BEGAN, state: State.ACTIVE },
+ { oldState: State.ACTIVE, state: State.END },
+ ]);
+ });
+
+ expect(pressFn).toHaveBeenCalledTimes(1);
+ });
+
+ test('does not call onPress on cancelled gesture', () => {
+ const pressFn = jest.fn();
+
+ const Example = () => (
+
+
+
+ );
+
+ render();
+ const gesture = getByGestureTestId('clickable');
+
+ act(() => {
+ fireGestureHandler(gesture, [
+ { oldState: State.UNDETERMINED, state: State.BEGAN },
+ { oldState: State.BEGAN, state: State.ACTIVE },
+ { oldState: State.ACTIVE, state: State.FAILED },
+ ]);
+ });
+
+ expect(pressFn).not.toHaveBeenCalled();
+ });
+
+ test('calls onActiveStateChange with correct values', () => {
+ const activeStateFn = jest.fn();
+
+ const Example = () => (
+
+
+
+ );
+
+ render();
+ const gesture = getByGestureTestId('clickable');
+
+ act(() => {
+ fireGestureHandler(gesture, [
+ { oldState: State.UNDETERMINED, state: State.BEGAN },
+ { oldState: State.BEGAN, state: State.ACTIVE },
+ { oldState: State.ACTIVE, state: State.END },
+ ]);
+ });
+
+ expect(activeStateFn).toHaveBeenCalledTimes(2);
+ expect(activeStateFn).toHaveBeenNthCalledWith(1, true);
+ expect(activeStateFn).toHaveBeenNthCalledWith(2, false);
+ });
+
+ test('calls onLongPress after delayLongPress and suppresses onPress', () => {
+ jest.useFakeTimers();
+
+ const pressFn = jest.fn();
+ const longPressFn = jest.fn();
+ const DELAY = 800;
+
+ const Example = () => (
+
+
+
+ );
+
+ render();
+
+ const gesture = getByGestureTestId('clickable') as SingleGesture<
+ any,
+ any,
+ any
+ >;
+ const { jsEventHandler } = gesture.detectorCallbacks;
+
+ // Fire BEGAN
+ act(() => {
+ jsEventHandler?.({
+ oldState: State.UNDETERMINED,
+ state: State.BEGAN,
+ handlerTag: gesture.handlerTag,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ handlerData: { pointerInside: true, numberOfPointers: 1 } as any,
+ });
+ });
+
+ // Fire ACTIVE — long press timer starts here (on iOS / non-Android)
+ act(() => {
+ jsEventHandler?.({
+ oldState: State.BEGAN,
+ state: State.ACTIVE,
+ handlerTag: gesture.handlerTag,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ handlerData: { pointerInside: true, numberOfPointers: 1 } as any,
+ });
+ });
+
+ expect(longPressFn).not.toHaveBeenCalled();
+
+ // Advance fake timers past delayLongPress
+ act(() => {
+ jest.advanceTimersByTime(DELAY);
+ });
+
+ expect(longPressFn).toHaveBeenCalledTimes(1);
+ expect(pressFn).not.toHaveBeenCalled();
+
+ // Fire END — onPress should be suppressed because long press was detected
+ act(() => {
+ jsEventHandler?.({
+ oldState: State.ACTIVE,
+ state: State.END,
+ handlerTag: gesture.handlerTag,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ handlerData: { pointerInside: true, numberOfPointers: 1 } as any,
+ });
+ });
+
+ expect(pressFn).not.toHaveBeenCalled();
+
+ jest.useRealTimers();
+ });
+ });
});
diff --git a/packages/react-native-gesture-handler/src/v3/components/Clickable/Clickable.tsx b/packages/react-native-gesture-handler/src/v3/components/Clickable/Clickable.tsx
new file mode 100644
index 0000000000..e8fbfdea8f
--- /dev/null
+++ b/packages/react-native-gesture-handler/src/v3/components/Clickable/Clickable.tsx
@@ -0,0 +1,197 @@
+import React, { useCallback, useMemo, useRef } from 'react';
+import { Animated, Platform, StyleSheet } from 'react-native';
+import { RawButton } from '../GestureButtons';
+import { CallbackEventType, ClickableProps } from './ClickableProps';
+
+const AnimatedRawButton = Animated.createAnimatedComponent(RawButton);
+const isAndroid = Platform.OS === 'android';
+const TRANSPARENT_RIPPLE = { rippleColor: 'transparent' as const };
+
+export const Clickable = (props: ClickableProps) => {
+ const {
+ underlayColor,
+ underlayInitialOpacity = 0,
+ underlayActiveOpacity,
+ initialOpacity = 1,
+ activeOpacity,
+ androidRipple,
+ delayLongPress = 600,
+ onLongPress,
+ onPress,
+ onPressIn,
+ onPressOut,
+ onActiveStateChange,
+ style,
+ children,
+ ref,
+ ...rest
+ } = props;
+
+ const animatedValue = useRef(new Animated.Value(0)).current;
+
+ const shouldAnimateUnderlay = underlayActiveOpacity !== undefined;
+ const shouldAnimateComponent = activeOpacity !== undefined;
+
+ const shouldUseNativeRipple = isAndroid && androidRipple !== undefined;
+ const shouldUseJSAnimation = shouldAnimateComponent || shouldAnimateUnderlay;
+
+ const longPressDetected = useRef(false);
+ const longPressTimeout = useRef | undefined>(
+ undefined
+ );
+
+ const wrappedLongPress = useCallback(() => {
+ longPressDetected.current = true;
+ onLongPress?.();
+ }, [onLongPress]);
+
+ const startLongPressTimer = useCallback(() => {
+ longPressDetected.current = false;
+
+ if (onLongPress && !longPressTimeout.current) {
+ longPressTimeout.current = setTimeout(wrappedLongPress, delayLongPress);
+ }
+ }, [onLongPress, delayLongPress, wrappedLongPress]);
+
+ const onBegin = useCallback(
+ (e: CallbackEventType) => {
+ if (!isAndroid || !e.pointerInside) {
+ return;
+ }
+
+ onPressIn?.(e);
+ startLongPressTimer();
+
+ if (shouldUseJSAnimation) {
+ animatedValue.setValue(1);
+ }
+ },
+ [startLongPressTimer, shouldUseJSAnimation, animatedValue, onPressIn]
+ );
+
+ const onActivate = useCallback(
+ (e: CallbackEventType) => {
+ onActiveStateChange?.(true);
+
+ if (!isAndroid) {
+ if (e.pointerInside) {
+ onPressIn?.(e);
+ startLongPressTimer();
+
+ if (shouldUseJSAnimation) {
+ animatedValue.setValue(1);
+ }
+ }
+ }
+
+ if (!e.pointerInside && longPressTimeout.current !== undefined) {
+ clearTimeout(longPressTimeout.current);
+ longPressTimeout.current = undefined;
+ }
+ },
+ [
+ onActiveStateChange,
+ shouldUseJSAnimation,
+ animatedValue,
+ startLongPressTimer,
+ onPressIn,
+ ]
+ );
+
+ const onDeactivate = useCallback(
+ (e: CallbackEventType, success: boolean) => {
+ onActiveStateChange?.(false);
+
+ if (success && !longPressDetected.current) {
+ onPress?.(e.pointerInside);
+ }
+ },
+ [onActiveStateChange, onPress]
+ );
+
+ const onFinalize = useCallback(
+ (e: CallbackEventType) => {
+ if (shouldUseJSAnimation) {
+ animatedValue.setValue(0);
+ }
+
+ onPressOut?.(e);
+
+ if (longPressTimeout.current !== undefined) {
+ clearTimeout(longPressTimeout.current);
+ longPressTimeout.current = undefined;
+ }
+ },
+ [shouldUseJSAnimation, animatedValue, onPressOut]
+ );
+
+ const underlayAnimatedStyle = useMemo(() => {
+ return shouldAnimateUnderlay
+ ? {
+ opacity: animatedValue.interpolate({
+ inputRange: [0, 1],
+ outputRange: [underlayInitialOpacity, underlayActiveOpacity],
+ }),
+ backgroundColor: underlayColor ?? 'black',
+ }
+ : undefined;
+ }, [
+ shouldAnimateUnderlay,
+ style,
+ underlayInitialOpacity,
+ underlayActiveOpacity,
+ underlayColor,
+ animatedValue,
+ ]);
+
+ const componentAnimatedStyle = useMemo(
+ () =>
+ shouldAnimateComponent
+ ? {
+ opacity: animatedValue.interpolate({
+ inputRange: [0, 1],
+ outputRange: [initialOpacity, activeOpacity],
+ }),
+ }
+ : undefined,
+ [shouldAnimateComponent, activeOpacity, animatedValue, initialOpacity]
+ );
+
+ const rippleProps = shouldUseNativeRipple
+ ? {
+ rippleColor: androidRipple?.color,
+ rippleRadius: androidRipple?.radius,
+ borderless: androidRipple?.borderless,
+ foreground: androidRipple?.foreground,
+ }
+ : TRANSPARENT_RIPPLE;
+
+ const ButtonComponent = shouldUseJSAnimation ? AnimatedRawButton : RawButton;
+
+ return (
+
+ {underlayAnimatedStyle && (
+
+ )}
+ {children}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ underlay: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ bottom: 0,
+ top: 0,
+ },
+});
diff --git a/packages/react-native-gesture-handler/src/v3/components/Clickable/ClickableProps.ts b/packages/react-native-gesture-handler/src/v3/components/Clickable/ClickableProps.ts
new file mode 100644
index 0000000000..f837742acb
--- /dev/null
+++ b/packages/react-native-gesture-handler/src/v3/components/Clickable/ClickableProps.ts
@@ -0,0 +1,59 @@
+import type { PressableAndroidRippleConfig as RNPressableAndroidRippleConfig } from 'react-native';
+import type { BaseButtonProps } from '../GestureButtonsProps';
+import type { GestureEvent } from '../../types';
+import type { NativeHandlerData } from '../../hooks/gestures/native/NativeTypes';
+
+export type CallbackEventType = GestureEvent;
+
+type PressableAndroidRippleConfig = {
+ [K in keyof RNPressableAndroidRippleConfig]?: Exclude<
+ RNPressableAndroidRippleConfig[K],
+ null
+ >;
+};
+
+type RippleProps = 'rippleColor' | 'rippleRadius' | 'borderless' | 'foreground';
+
+export interface ClickableProps extends Omit {
+ /**
+ * Background color of underlay. Requires `underlayActiveOpacity` to be set.
+ */
+ underlayColor?: string | undefined;
+
+ /**
+ * Opacity applied to the underlay when it is in an active state.
+ * If not provided, no visual feedback will be applied.
+ */
+ underlayActiveOpacity?: number | undefined;
+
+ /**
+ * Opacity applied to the component when it is in an active state.
+ * If not provided, no visual feedback will be applied.
+ */
+ activeOpacity?: number | undefined;
+
+ /**
+ * Initial opacity of the underlay.
+ */
+ underlayInitialOpacity?: number | undefined;
+
+ /**
+ * Initial opacity of the component.
+ */
+ initialOpacity?: number | undefined;
+
+ /**
+ * Configuration for the ripple effect on Android.
+ */
+ androidRipple?: PressableAndroidRippleConfig | undefined;
+
+ /**
+ * Called when pointer touches the component.
+ */
+ onPressIn?: ((event: CallbackEventType) => void) | undefined;
+
+ /**
+ * Called when pointer is released from the component.
+ */
+ onPressOut?: ((event: CallbackEventType) => void) | undefined;
+}
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 fc6eee6ebc..c8cbd0f710 100644
--- a/packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx
+++ b/packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx
@@ -5,6 +5,7 @@ import GestureHandlerButton from '../../components/GestureHandlerButton';
import type {
BaseButtonProps,
BorderlessButtonProps,
+ RawButtonProps,
RectButtonProps,
} from './GestureButtonsProps';
@@ -13,11 +14,17 @@ import type { NativeHandlerData } from '../hooks/gestures/native/NativeTypes';
type CallbackEventType = GestureEvent;
-export const RawButton = createNativeWrapper(GestureHandlerButton, {
+export const RawButton = createNativeWrapper<
+ ReturnType,
+ RawButtonProps
+>(GestureHandlerButton, {
shouldCancelWhenOutside: false,
shouldActivateOnStart: false,
});
+/**
+ * @deprecated `BaseButton` is deprecated, use `Clickable` instead
+ */
export const BaseButton = (props: BaseButtonProps) => {
const longPressDetected = useRef(false);
const longPressTimeout = useRef | undefined>(
@@ -97,6 +104,9 @@ const btnStyles = StyleSheet.create({
},
});
+/**
+ * @deprecated `RectButton` is deprecated, use `Clickable` with `underlayActiveOpacity={0.7}` instead
+ */
export const RectButton = (props: RectButtonProps) => {
const activeOpacity = props.activeOpacity ?? 0.105;
const underlayColor = props.underlayColor ?? 'black';
@@ -139,6 +149,9 @@ export const RectButton = (props: RectButtonProps) => {
);
};
+/**
+ * @deprecated `BorderlessButton` is deprecated, use `Clickable` with `activeOpacity={0.3}` instead
+ */
export const BorderlessButton = (props: BorderlessButtonProps) => {
const activeOpacity = props.activeOpacity ?? 0.3;
const opacity = useRef(new Animated.Value(1)).current;
@@ -151,11 +164,12 @@ export const BorderlessButton = (props: BorderlessButtonProps) => {
props.onActiveStateChange?.(active);
};
- const { children, style, ...rest } = props;
+ const { children, style, ref, ...rest } = props;
return (
{children}
diff --git a/packages/react-native-gesture-handler/src/v3/components/index.ts b/packages/react-native-gesture-handler/src/v3/components/index.ts
index bf4bbc5526..30adc491c4 100644
--- a/packages/react-native-gesture-handler/src/v3/components/index.ts
+++ b/packages/react-native-gesture-handler/src/v3/components/index.ts
@@ -22,3 +22,6 @@ export {
} from './GestureComponents';
export { default as Pressable } from './Pressable';
+
+export { Clickable } from './Clickable/Clickable';
+export type { ClickableProps } from './Clickable/ClickableProps';
diff --git a/packages/react-native-gesture-handler/src/v3/index.ts b/packages/react-native-gesture-handler/src/v3/index.ts
index 3c720e10dd..50655f9cbe 100644
--- a/packages/react-native-gesture-handler/src/v3/index.ts
+++ b/packages/react-native-gesture-handler/src/v3/index.ts
@@ -66,7 +66,9 @@ export type {
BaseButtonProps,
RectButtonProps,
BorderlessButtonProps,
+ ClickableProps,
} from './components';
+
export {
RawButton,
BaseButton,
@@ -78,6 +80,7 @@ export {
TextInput,
FlatList,
RefreshControl,
+ Clickable,
} from './components';
export type { ComposedGesture } from './types';
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 c7cb30c78f..7ffc10ecd8 100644
--- a/packages/react-native-gesture-handler/src/v3/types/NativeWrapperType.ts
+++ b/packages/react-native-gesture-handler/src/v3/types/NativeWrapperType.ts
@@ -7,7 +7,7 @@ import {
} from '../hooks/gestures/native/NativeTypes';
export type WrapperSpecificProperties = {
- ref?: React.Ref;
+ ref?: React.Ref | undefined;
onGestureUpdate_CAN_CAUSE_INFINITE_RERENDER?: (
gesture: NativeGesture
) => void;