Conversation
## Description This PR introduces new `Clickable` component, which is meant to be a replacement for buttons. > [!NOTE] > Docs for `Clickable` will be added in #4022, as I don't want to release them right away after merging this PR. ### `borderless` For now, `borderless` doesn't work. I've tested clickable with some changes that allow `borderless` ripple to be visible, however we don't want to introduce them here because it would break other things. Also it should be generl fix, not in the PR with new component. ## Stress test Render list with 2000 buttons 25 times (50ms delay between renders), drop 3 best and 3 worst results. Then calculate average. Stress test example is available in this PR. #### Android | | $t_{Button}$ | $t_{Clickable}$ | $\Delta{t}$ | |------------------|--------------|-----------------|--------------| | `BaseButton` | 1196,18 | 1292,3 | 96,12 | | `RectButton` | 1672,6 | 1275,68 | -396,92 | | `BorderlessButton` | 1451,34 | 1290,74 | -160,6 | #### iOS | | $t_{Button}$ | $t_{Clickable}$ | $\Delta{t}$ | |------------------|--------------|-----------------|--------------| | `BaseButton` | 1101,37 | 1154,6 | 53,23 | | `RectButton` | 1528,07 | 1160,07 | -368 | | `BorderlessButton` | 1330,24 | 1172,69 | -157,55 | #### Web | | $t_{Button}$ | $t_{Clickable}$ | $\Delta{t}$ | |------------------|--------------|-----------------|--------------| | `BaseButton` | 64,18 | 95,57 | 31,39 | | `RectButton` | 104,58 | 97,95 | -6,63 | | `BorderlessButton` | 81,11 | 98,64 | 17,53 | ## Test plan New examples --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
|
||
| `PureNativeButton` has been removed. If encountered, inform the user that it has been removed and let them decide how to handle that case. They can achieve similar functionality with other buttons. | ||
|
|
||
| When migrating buttons, you should use `Clickable` component instead. To replace `BaseButton` use `Clickable` with default props, to replace `RectButton` use `Clickable` with `underlayActiveOpacity={0.105}` and to replace `BorderlessButton` use `Clickable` with `activeOpacity={0.3}`. |
There was a problem hiding this comment.
It may also be worth adding how to replace deprecated Touchables with the new component.
There was a problem hiding this comment.
Okay, what about d3c4337?
Also, I've tested it on the code below and seems like there are some android-related issues, I'll check what it is and let you know later.
Test code
import React from 'react';
import { StyleSheet, Text, View, ScrollView, Platform } from 'react-native';
import {
GestureHandlerRootView,
Clickable,
TouchableOpacity,
TouchableHighlight,
TouchableWithoutFeedback,
TouchableNativeFeedback,
} from 'react-native-gesture-handler';
import { COLORS } from '../../../common';
function SectionHeader({ children }: { children: string }) {
return <Text style={styles.sectionHeader}>{children}</Text>;
}
function Label({ children }: { children: string }) {
return <Text style={styles.label}>{children}</Text>;
}
function ComparisonRow({
before,
after,
}: {
before: React.ReactNode;
after: React.ReactNode;
}) {
return (
<View style={styles.comparisonRow}>
<View style={styles.comparisonColumn}>
<Text style={styles.comparisonLabel}>Before</Text>
{before}
</View>
<View style={styles.comparisonColumn}>
<Text style={styles.comparisonLabel}>After</Text>
{after}
</View>
</View>
);
}
function ButtonContent({ text, color }: { text: string; color: string }) {
return (
<View style={[styles.button, { backgroundColor: color }]}>
<Text style={styles.buttonText}>{text}</Text>
</View>
);
}
export default function ClickableTouchableMigrationExample() {
return (
<GestureHandlerRootView style={styles.container}>
<ScrollView contentContainerStyle={styles.scrollContent}>
{/* TouchableOpacity → Clickable */}
<View style={styles.section}>
<SectionHeader>TouchableOpacity → Clickable</SectionHeader>
<Label>Uses activeOpacity to dim the component on press.</Label>
<ComparisonRow
before={
<TouchableOpacity
onPress={() => console.log('TouchableOpacity pressed')}>
<ButtonContent text="TouchableOpacity" color={COLORS.NAVY} />
</TouchableOpacity>
}
after={
<Clickable
activeOpacity={0.2}
onPress={() => console.log('Clickable (opacity) pressed')}>
<ButtonContent text="Clickable" color={COLORS.NAVY} />
</Clickable>
}
/>
</View>
{/* TouchableHighlight → Clickable */}
<View style={styles.section}>
<SectionHeader>TouchableHighlight → Clickable</SectionHeader>
<Label>
Uses underlayColor and activeUnderlayOpacity to show a colored
underlay on press.
</Label>
<ComparisonRow
before={
<TouchableHighlight
// underlayColor={COLORS.KINDA_BLUE}
onPress={() => console.log('TouchableHighlight pressed')}>
<ButtonContent
text="TouchableHighlight"
color={COLORS.WEB_BLUE}
/>
</TouchableHighlight>
}
after={
<Clickable
// underlayColor={COLORS.KINDA_BLUE}
activeUnderlayOpacity={1}
onPress={() => console.log('Clickable (highlight) pressed')}>
<ButtonContent text="Clickable" color={COLORS.WEB_BLUE} />
</Clickable>
}
/>
</View>
{/* TouchableWithoutFeedback → Clickable */}
<View style={styles.section}>
<SectionHeader>TouchableWithoutFeedback → Clickable</SectionHeader>
<Label>No visual feedback — just handles press events.</Label>
<ComparisonRow
before={
<TouchableWithoutFeedback
onPress={() => console.log('TouchableWithoutFeedback pressed')}>
<ButtonContent
text="WithoutFeedback"
color={COLORS.DARK_GREEN}
/>
</TouchableWithoutFeedback>
}
after={
<Clickable
onPress={() => console.log('Clickable (no feedback) pressed')}>
<ButtonContent text="Clickable" color={COLORS.DARK_GREEN} />
</Clickable>
}
/>
</View>
{/* TouchableNativeFeedback → Clickable (Android only) */}
{Platform.OS === 'android' && (
<View style={styles.section}>
<SectionHeader>TouchableNativeFeedback → Clickable</SectionHeader>
<Label>
Uses androidRipple to show the native ripple effect (Android
only).
</Label>
<ComparisonRow
before={
<TouchableNativeFeedback
// background={TouchableNativeFeedback.Ripple('#aaa', false)}
onPress={() =>
console.log('TouchableNativeFeedback pressed')
}>
<ButtonContent text="NativeFeedback" color={COLORS.ANDROID} />
</TouchableNativeFeedback>
}
after={
<Clickable
androidRipple={{
foreground: true,
}}
onPress={() => console.log('Clickable (ripple) pressed')}>
<ButtonContent text="Clickable" color={COLORS.ANDROID} />
</Clickable>
}
/>
</View>
)}
</ScrollView>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
scrollContent: {
paddingBottom: 40,
},
section: {
padding: 20,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#ccc',
},
sectionHeader: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 4,
},
label: {
fontSize: 13,
color: '#666',
marginBottom: 12,
},
comparisonRow: {
flexDirection: 'row',
gap: 12,
},
comparisonColumn: {
flex: 1,
alignItems: 'center',
},
comparisonLabel: {
fontSize: 12,
fontWeight: '600',
color: '#999',
marginBottom: 8,
textTransform: 'uppercase',
},
button: {
width: '100%',
height: 50,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
},
buttonText: {
color: 'white',
fontSize: 13,
fontWeight: '600',
},
});There was a problem hiding this comment.
- Add this to the LLM skill as well
- This will read weird if we settle on
Touchableas a name
There was a problem hiding this comment.
Ad 1. 46eba22
Ad 2. I agree. I've changed this section (it is not commited though, I've put this below so you can read it). Let me know what you think.
Rewritten section
Migrating from legacy Touchable variants
If you were using the specialized touchable components (TouchableOpacity, TouchableHighlight, TouchableWithoutFeedback, or TouchableNativeFeedback), you can replicate their behavior with the unified Touchable component.
TouchableOpacity
To replicate TouchableOpacity behavior, add activeOpacity={0.2}.
<Touchable
...
activeOpacity={0.2}/>TouchableHighlight
To replicate TouchableHighlight behavior, add activeUnderlayOpacity={1}.
<Touchable
...
activeUnderlayOpacity={1}/>TouchableWithoutFeedback
To replicate TouchableWithoutFeedback behavior, use a plain Touchable.
<Touchable ... />TouchableNativeFeedback
To replicate TouchableNativeFeedback behavior, use the androidRipple prop. Make sure to set foreground={true}.
<Touchable
...
androidRipple={{
foreground: true,
}}/>There was a problem hiding this comment.
Okay, following #4063 I've renamed Clickable to Touchable
Co-authored-by: Jakub Piasecki <jakub.piasecki@swmansion.com>
Co-authored-by: Jakub Piasecki <jakub.piasecki@swmansion.com>
…eact-native-gesture-handler into @mbert/clickable-docs
Co-authored-by: Jakub Piasecki <jakub.piasecki@swmansion.com>
Co-authored-by: Jakub Piasecki <jakub.piasecki@swmansion.com>
Co-authored-by: Jakub Piasecki <jakub.piasecki@swmansion.com>
Co-authored-by: Jakub Piasecki <jakub.piasecki@swmansion.com>
…eact-native-gesture-handler into @mbert/clickable-docs
Description
This PR adds docs for
ClickablecomponentTest plan
Read docs 🤓