From ff2b5b70fe234e78a1ed59e87de09919a2177f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Tue, 18 Aug 2026 14:51:58 +0200 Subject: [PATCH] Pressable ripple --- .../src/__tests__/pressableRipple.test.tsx | 72 +++++++++++++++++++ .../src/components/Pressable/Pressable.tsx | 2 + .../src/v3/components/StatefulPressable.tsx | 2 + 3 files changed, 76 insertions(+) create mode 100644 packages/react-native-gesture-handler/src/__tests__/pressableRipple.test.tsx diff --git a/packages/react-native-gesture-handler/src/__tests__/pressableRipple.test.tsx b/packages/react-native-gesture-handler/src/__tests__/pressableRipple.test.tsx new file mode 100644 index 0000000000..3f8d923b10 --- /dev/null +++ b/packages/react-native-gesture-handler/src/__tests__/pressableRipple.test.tsx @@ -0,0 +1,72 @@ +import { render, screen } from '@testing-library/react-native'; +import React from 'react'; +import type { Platform as PlatformModule } from 'react-native'; +import { Text } from 'react-native'; + +import GestureHandlerRootView from '../components/GestureHandlerRootView'; +import { Pressable } from '../v3/components'; + +// The Touchable engine reads `Platform.OS` once, at import time, to decide +// whether the native ripple applies — so the platform has to be swapped before +// the component module loads. +jest.mock('react-native/Libraries/Utilities/Platform', () => { + const actual = jest.requireActual<{ default: typeof PlatformModule }>( + 'react-native/Libraries/Utilities/Platform' + ).default; + return { + __esModule: true, + default: { + ...actual, + OS: 'android', + select: (spec: Record) => + 'android' in spec ? spec.android : (spec.native ?? spec.default), + }, + }; +}); + +// Both engines have to forward the whole `android_ripple` config to the button — +// dropping `borderless`/`foreground` made the two flags silently do nothing on +// the engine that omitted them. +const RIPPLE = { + color: 'red', + radius: 20, + borderless: true, + foreground: true, +} as const; + +const expectRippleOnButton = () => { + const button = screen.getByTestId('pressable'); + + expect(button.props.rippleColor).toBe(RIPPLE.color); + expect(button.props.rippleRadius).toBe(RIPPLE.radius); + expect(button.props.borderless).toBe(true); + expect(button.props.foreground).toBe(true); +}; + +test('StatefulPressable forwards the whole android_ripple config', () => { + render( + + {/* A relation prop routes `Pressable` to the `StatefulPressable` engine. */} + + Press Me + + + ); + + expectRippleOnButton(); +}); + +test('PressableWithTouchable forwards the whole android_ripple config', () => { + render( + + + Press Me + + + ); + + expectRippleOnButton(); +}); diff --git a/packages/react-native-gesture-handler/src/components/Pressable/Pressable.tsx b/packages/react-native-gesture-handler/src/components/Pressable/Pressable.tsx index 4828a481d6..cdd52cda0f 100644 --- a/packages/react-native-gesture-handler/src/components/Pressable/Pressable.tsx +++ b/packages/react-native-gesture-handler/src/components/Pressable/Pressable.tsx @@ -395,6 +395,8 @@ const LegacyPressable = (props: LegacyPressableProps) => { touchSoundDisabled={android_disableSound ?? undefined} rippleColor={rippleColor} rippleRadius={android_ripple?.radius ?? undefined} + borderless={android_ripple?.borderless ?? undefined} + foreground={android_ripple?.foreground ?? undefined} style={[pointerStyle, styleProp]} testOnly_onPress={IS_TEST_ENV ? onPress : undefined} testOnly_onPressIn={IS_TEST_ENV ? onPressIn : undefined} diff --git a/packages/react-native-gesture-handler/src/v3/components/StatefulPressable.tsx b/packages/react-native-gesture-handler/src/v3/components/StatefulPressable.tsx index 38fc07b022..5be685f22e 100644 --- a/packages/react-native-gesture-handler/src/v3/components/StatefulPressable.tsx +++ b/packages/react-native-gesture-handler/src/v3/components/StatefulPressable.tsx @@ -459,6 +459,8 @@ const StatefulPressable = (props: PressableProps) => { touchSoundDisabled={android_disableSound ?? undefined} rippleColor={rippleColor} rippleRadius={android_ripple?.radius ?? undefined} + borderless={android_ripple?.borderless ?? undefined} + foreground={android_ripple?.foreground ?? undefined} style={[pointerStyle, styleProp]} testOnly_onPress={IS_TEST_ENV ? onPress : undefined} testOnly_onPressIn={IS_TEST_ENV ? onPressIn : undefined}