Forward `borderless` and `foreground` from `android_ripple` (#4442)
## Description
Follow-up to #4411. `StatefulPressable` and the legacy `Pressable` only
forwarded `color` and `radius` from `android_ripple` to the button, so
`borderless` and `foreground` silently did nothing on those engines -
the ripple stayed bounded and drew under the children.
`PressableWithTouchable` already passed all four fields, which made the
behavior depend on whether a relation prop
(`simultaneousWith`/`requireToFail`/`block`) was present.
Both engines now pass the whole config through. The button props for the
two flags already existed, only the Pressable side dropped them.
## Test plan
<details>
<summary>Tested on the following code:</summary>
```tsx
import React from 'react';
import {
Pressable as RNPressable,
ScrollView,
StyleSheet,
Text,
View,
} from 'react-native';
import { LegacyPressable, Pressable } from 'react-native-gesture-handler';
import type { PressableProps } from 'react-native-gesture-handler';
// Android-only check for `android_ripple.borderless` / `.foreground`, which the
// StatefulPressable and legacy engines used to drop (only color + radius were
// forwarded to the button).
//
// borderless: the ripple is a circle that spills outside the box.
// foreground: the ripple draws over the opaque child instead of under it.
//
// Every column must look the same. Nothing to see on iOS - no native ripple there.
const RIPPLE_COLOR = '#1565c0';
const VARIANTS = [
{
label: 'baseline\n{ color }',
ripple: { color: RIPPLE_COLOR },
covered: false,
},
{
label: 'borderless\n{ borderless: true }',
ripple: { color: RIPPLE_COLOR, borderless: true },
covered: false,
},
{
// Control for the row below: a background ripple hides under the child.
label: 'covered, control\n{ color }\nno ripple expected',
ripple: { color: RIPPLE_COLOR },
covered: true,
},
{
label: 'foreground\n{ foreground: true }\nripple over the child',
ripple: { color: RIPPLE_COLOR, foreground: true },
covered: true,
},
{
label: 'both + radius\n{ borderless, foreground, radius: 70 }',
ripple: {
color: RIPPLE_COLOR,
borderless: true,
foreground: true,
radius: 70,
},
covered: true,
},
] as const;
// A relation prop is what routes the public `Pressable` to the Stateful engine.
function StatefulPressable(props: PressableProps) {
return <Pressable {...props} simultaneousWith={[]} />;
}
const ENGINES = [
{ label: 'v3\nTouchable', Component: Pressable },
{ label: 'v3\nStateful', Component: StatefulPressable },
{ label: 'legacy\nRNGH', Component: LegacyPressable },
{ label: 'RN\ncontrol', Component: RNPressable },
] as const;
export default function EmptyExample() {
return (
<ScrollView contentContainerStyle={styles.container}>
<View style={styles.row}>
<View style={styles.variantLabel} />
{ENGINES.map((engine) => (
<Text key={engine.label} style={styles.engineLabel}>
{engine.label}
</Text>
))}
</View>
{VARIANTS.map((variant) => (
<View key={variant.label} style={styles.row}>
<Text style={[styles.variantLabel, styles.variantText]}>
{variant.label}
</Text>
{ENGINES.map(({ label, Component }) => (
<View key={label} style={styles.cell}>
<Component android_ripple={variant.ripple} style={styles.button}>
{variant.covered ? <View style={styles.cover} /> : null}
</Component>
</View>
))}
</View>
))}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
padding: 12,
paddingTop: 40,
gap: 20,
},
row: {
flexDirection: 'row',
alignItems: 'center',
},
variantLabel: {
width: 100,
},
variantText: {
fontSize: 11,
fontFamily: 'monospace',
color: '#37474f',
},
engineLabel: {
flex: 1,
fontSize: 11,
textAlign: 'center',
color: '#607d8b',
},
cell: {
flex: 1,
alignItems: 'center',
},
button: {
width: 54,
height: 54,
borderRadius: 6,
backgroundColor: '#eceff1',
},
cover: {
flex: 1,
backgroundColor: '#cfd8dc',
},
});
```
</details> M
Michał Bert committed
b2bbb4974d66f6a95c2447f5dceacf91d621604f
Parent: 00acc10
Committed by GitHub <noreply@github.com>
on 8/19/2026, 10:52:03 AM