Skip to content

Commit 7e938cf

Browse files
fix: Button and IconButton TouchableRipple borderRadius (#4278)
1 parent c5161ab commit 7e938cf

File tree

16 files changed

+182
-105
lines changed

16 files changed

+182
-105
lines changed

example/src/Examples/ButtonExample.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,14 @@ const ButtonExample = () => {
296296
<Button mode="contained" onPress={() => {}} style={styles.noRadius}>
297297
Without radius
298298
</Button>
299+
<Button
300+
mode="contained-tonal"
301+
onPress={() => {}}
302+
style={{ borderRadius: styles.customRadiusAndPadding.borderRadius }}
303+
contentStyle={styles.customRadiusAndPadding}
304+
>
305+
Custom radius and padding
306+
</Button>
299307
</View>
300308

301309
<View style={styles.row}>
@@ -355,6 +363,7 @@ const styles = StyleSheet.create({
355363
flexWrap: 'wrap',
356364
paddingHorizontal: 12,
357365
alignItems: 'center',
366+
gap: 12,
358367
},
359368
button: {
360369
margin: 4,
@@ -386,6 +395,11 @@ const styles = StyleSheet.create({
386395
noRadius: {
387396
borderRadius: 0,
388397
},
398+
customRadiusAndPadding: {
399+
borderRadius: 4,
400+
paddingHorizontal: 12,
401+
paddingVertical: 6,
402+
},
389403
});
390404

391405
export default ButtonExample;

example/src/Examples/IconButtonExample.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,31 @@ const ButtonExample = () => {
157157
iconColor={MD3Colors.tertiary50}
158158
onPress={() => {}}
159159
/>
160+
<IconButton
161+
icon="eye"
162+
mode="contained"
163+
style={styles.slightlyRounded}
164+
size={24}
165+
contentStyle={{ padding: 8 }}
166+
iconColor={MD3Colors.tertiary50}
167+
onPress={() => {}}
168+
/>
169+
<IconButton
170+
icon="heart"
171+
mode="contained-tonal"
172+
style={styles.differentBorderRadius}
173+
size={24}
174+
iconColor={MD3Colors.tertiary50}
175+
onPress={() => {}}
176+
/>
177+
<IconButton
178+
icon="heart"
179+
mode="outlined"
180+
style={styles.differentBorderRadius}
181+
size={24}
182+
iconColor={MD3Colors.tertiary50}
183+
onPress={() => {}}
184+
/>
160185
<IconButton icon="camera" size={36} onPress={() => {}} />
161186
<IconButton
162187
icon="lock"
@@ -189,6 +214,17 @@ const styles = StyleSheet.create({
189214
square: {
190215
borderRadius: 0,
191216
},
217+
slightlyRounded: {
218+
borderRadius: 4,
219+
width: 48,
220+
height: 48,
221+
},
222+
differentBorderRadius: {
223+
borderTopLeftRadius: 2,
224+
borderTopRightRadius: 4,
225+
borderBottomLeftRadius: 8,
226+
borderBottomRightRadius: 6,
227+
},
192228
});
193229

194230
export default ButtonExample;

src/components/Button/Button.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ import {
1515

1616
import color from 'color';
1717

18-
import { ButtonMode, getButtonColors } from './utils';
18+
import {
19+
ButtonMode,
20+
getButtonColors,
21+
getButtonTouchableRippleStyle,
22+
} from './utils';
1923
import { useInternalTheme } from '../../core/theming';
2024
import type { $Omit, ThemeProp } from '../../types';
2125
import { forwardRef } from '../../utils/forwardRef';
@@ -123,7 +127,7 @@ export type Props = $Omit<React.ComponentProps<typeof Surface>, 'mode'> & {
123127
delayLongPress?: number;
124128
/**
125129
* Style of button's inner content.
126-
* Use this prop to apply custom height and width and to set the icon on the right with `flexDirection: 'row-reverse'`.
130+
* Use this prop to apply custom height and width, to set a custom padding or to set the icon on the right with `flexDirection: 'row-reverse'`.
127131
*/
128132
contentStyle?: StyleProp<ViewStyle>;
129133
/**
@@ -356,7 +360,7 @@ const Button = (
356360
accessible={accessible}
357361
disabled={disabled}
358362
rippleColor={rippleColor}
359-
style={touchableStyle}
363+
style={getButtonTouchableRippleStyle(touchableStyle, borderWidth)}
360364
testID={testID}
361365
theme={theme}
362366
ref={touchableRef}

src/components/Button/utils.tsx

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { StyleSheet } from 'react-native';
1+
import { StyleSheet, type ViewStyle } from 'react-native';
22

33
import color from 'color';
44

55
import { black, white } from '../../styles/themes/v2/colors';
66
import type { InternalTheme } from '../../types';
7+
import { splitStyles } from '../../utils/splitStyles';
78

89
export type ButtonMode =
910
| 'text'
@@ -230,3 +231,42 @@ export const getButtonColors = ({
230231
borderWidth,
231232
};
232233
};
234+
235+
type ViewStyleBorderRadiusStyles = Partial<
236+
Pick<
237+
ViewStyle,
238+
| 'borderBottomEndRadius'
239+
| 'borderBottomLeftRadius'
240+
| 'borderBottomRightRadius'
241+
| 'borderBottomStartRadius'
242+
| 'borderTopEndRadius'
243+
| 'borderTopLeftRadius'
244+
| 'borderTopRightRadius'
245+
| 'borderTopStartRadius'
246+
| 'borderRadius'
247+
>
248+
>;
249+
export const getButtonTouchableRippleStyle = (
250+
style?: ViewStyle,
251+
borderWidth: number = 0
252+
): ViewStyleBorderRadiusStyles => {
253+
if (!style) return {};
254+
const touchableRippleStyle: ViewStyleBorderRadiusStyles = {};
255+
256+
const [, borderRadiusStyles] = splitStyles(
257+
style,
258+
(style) => style.startsWith('border') && style.endsWith('Radius')
259+
);
260+
261+
(
262+
Object.keys(borderRadiusStyles) as Array<keyof ViewStyleBorderRadiusStyles>
263+
).forEach((key) => {
264+
const value = style[key as keyof ViewStyleBorderRadiusStyles];
265+
if (typeof value === 'number') {
266+
// Only subtract borderWidth if value is greater than 0
267+
const radius = value > 0 ? value - borderWidth : 0;
268+
touchableRippleStyle[key as keyof ViewStyleBorderRadiusStyles] = radius;
269+
}
270+
});
271+
return touchableRippleStyle;
272+
};

src/components/IconButton/IconButton.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ export type Props = Omit<$RemoveChildren<typeof TouchableRipple>, 'style'> & {
6767
* Accessibility label for the button. This is read by the screen reader when the user taps the button.
6868
*/
6969
accessibilityLabel?: string;
70+
/**
71+
* Style of button's inner content.
72+
* Use this prop to apply custom height and width or to set a custom padding`.
73+
*/
74+
contentStyle?: StyleProp<ViewStyle>;
7075
/**
7176
* Function to execute on press.
7277
*/
@@ -127,6 +132,7 @@ const IconButton = forwardRef<View, Props>(
127132
theme: themeOverrides,
128133
testID = 'icon-button',
129134
loading = false,
135+
contentStyle,
130136
...rest
131137
}: Props,
132138
ref
@@ -183,7 +189,7 @@ const IconButton = forwardRef<View, Props>(
183189
onPress={onPress}
184190
rippleColor={rippleColor}
185191
accessibilityLabel={accessibilityLabel}
186-
style={[styles.touchable, { borderRadius }]}
192+
style={[styles.touchable, contentStyle]}
187193
// @ts-expect-error We keep old a11y props for backwards compat with old RN versions
188194
accessibilityTraits={disabled ? ['button', 'disabled'] : 'button'}
189195
accessibilityComponentType="button"

src/components/TouchableRipple/TouchableRipple.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ const TouchableRipple = (
156156
// Get the size of the button to determine how big the ripple should be
157157
const size = centered
158158
? // If ripple is always centered, we don't need to make it too big
159-
Math.min(dimensions.width, dimensions.height) * 1.25
159+
Math.min(dimensions.width, dimensions.height) * 1.5
160160
: // Otherwise make it twice as big so clicking on one end spreads ripple to other
161161
Math.max(dimensions.width, dimensions.height) * 2;
162162

src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,7 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = `
175175
"flexGrow": 1,
176176
"justifyContent": "center",
177177
},
178-
{
179-
"borderRadius": 20,
180-
},
178+
undefined,
181179
],
182180
]
183181
}
@@ -368,9 +366,7 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = `
368366
"flexGrow": 1,
369367
"justifyContent": "center",
370368
},
371-
{
372-
"borderRadius": 20,
373-
},
369+
undefined,
374370
],
375371
]
376372
}
@@ -560,9 +556,7 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A
560556
"flexGrow": 1,
561557
"justifyContent": "center",
562558
},
563-
{
564-
"borderRadius": 20,
565-
},
559+
undefined,
566560
],
567561
]
568562
}
@@ -803,9 +797,7 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A
803797
"flexGrow": 1,
804798
"justifyContent": "center",
805799
},
806-
{
807-
"borderRadius": 20,
808-
},
800+
undefined,
809801
],
810802
]
811803
}

src/components/__tests__/Button.test.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,28 @@ it('renders button with custom border radius', () => {
159159
expect(getByTestId('custom-radius')).toHaveStyle(styles.customRadius);
160160
});
161161

162+
it('renders outlined button with custom border radius', () => {
163+
const { getByTestId } = render(
164+
<Button
165+
mode={'outlined'}
166+
testID="custom-radius"
167+
style={styles.customRadius}
168+
>
169+
Custom radius
170+
</Button>
171+
);
172+
173+
expect(getByTestId('custom-radius-container')).toHaveStyle(
174+
styles.customRadius
175+
);
176+
expect(getByTestId('custom-radius')).toHaveStyle({
177+
borderTopLeftRadius: 15, // styles.customRadius - 1px outline
178+
borderTopRightRadius: 0,
179+
borderBottomLeftRadius: 0,
180+
borderBottomRightRadius: 15, // styles.customRadius - 1px outline
181+
});
182+
});
183+
162184
it('renders button without border radius', () => {
163185
const { getByTestId } = render(
164186
<Button testID="custom-radius" style={styles.noRadius}>

src/components/__tests__/IconButton.test.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ const styles = StyleSheet.create({
1313
square: {
1414
borderRadius: 0,
1515
},
16+
slightlyRounded: {
17+
borderRadius: 4,
18+
},
1619
});
1720

1821
it('renders icon button by default', () => {
@@ -58,7 +61,21 @@ it('renders icon button with custom border radius', () => {
5861
/>
5962
);
6063

61-
expect(getByTestId('icon-button')).toHaveStyle({ borderRadius: 0 });
64+
expect(getByTestId('icon-button-container')).toHaveStyle({ borderRadius: 0 });
65+
});
66+
67+
it('renders icon button with small border radius', () => {
68+
const { getByTestId } = render(
69+
<IconButton
70+
icon="camera"
71+
testID="icon-button"
72+
size={36}
73+
onPress={() => {}}
74+
style={styles.slightlyRounded}
75+
/>
76+
);
77+
78+
expect(getByTestId('icon-button-container')).toHaveStyle({ borderRadius: 4 });
6279
});
6380

6481
describe('getIconButtonColor - icon color', () => {

src/components/__tests__/__snapshots__/Button.test.tsx.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1902,7 +1902,7 @@ exports[`renders outlined button with mode 1`] = `
19021902
"overflow": "hidden",
19031903
},
19041904
{
1905-
"borderRadius": 20,
1905+
"borderRadius": 19,
19061906
},
19071907
]
19081908
}

0 commit comments

Comments
 (0)