Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Add support for borderRadii on RectButton #2691

Merged
merged 14 commits into from
Jan 11, 2024
2 changes: 2 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import NestedTouchables from './release_tests/nestedTouchables';
import NestedButtons from './release_tests/nestedButtons';
import NestedGestureHandlerRootViewWithModal from './release_tests/nestedGHRootViewWithModal';
import RoundedButtons from './release_tests/roundedButtons';
import { PinchableBox } from './recipes/scaleAndRotate';
import PanAndScroll from './recipes/panAndScroll';
import { BottomSheet } from './showcase/bottomSheet';
Expand Down Expand Up @@ -115,6 +116,7 @@
{ name: 'Fling', component: Fling },
{ name: 'Combo', component: ComboWithGHScroll },
{ name: 'Touchables', component: TouchablesIndex as React.ComponentType },
{ name: 'Rounded buttons', component: RoundedButtons },
],
},
{
Expand Down Expand Up @@ -197,7 +199,7 @@
renderSectionHeader={({ section: { sectionTitle } }) => (
<Text style={styles.sectionTitle}>{sectionTitle}</Text>
)}
ItemSeparatorComponent={() => <View style={styles.separator} />}

Check warning on line 202 in example/src/App.tsx

View workflow job for this annotation

GitHub Actions / check (example)

Do not define components during render. React will see a new component type on every render and destroy the entire subtree’s DOM nodes and state (https://reactjs.org/docs/reconciliation.html#elements-of-different-types). Instead, move this component definition out of the parent component “MainScreen” and pass data as props. If you want to allow component creation in props, set allowAsProps option to true
/>
);
}
Expand Down
138 changes: 138 additions & 0 deletions example/src/release_tests/roundedButtons/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React from 'react';
import { View, StyleSheet, Text, SafeAreaView } from 'react-native';
import {
GestureHandlerRootView,
ScrollView,
RectButton,
} from 'react-native-gesture-handler';

const MyButton = RectButton;

export default function ComplexUI() {
return (
<GestureHandlerRootView style={styles.container}>
<SafeAreaView style={styles.container}>
<ScrollView>
<Avatars />
<View style={styles.paddedContainer}>
<Gallery />
<Gallery />
<Gallery />
<Gallery />
<Gallery />
</View>
</ScrollView>
</SafeAreaView>
</GestureHandlerRootView>
);
}
const colors = ['#782AEB', '#38ACDD', '#57B495', '#FF6259', '#FFD61E'];

function Avatars() {
return (
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
{colors.map((color) => (
<MyButton
key={color}
style={[styles.avatars, { backgroundColor: color }]}>
<Text style={styles.avatarLabel}>{color.slice(1, 3)}</Text>
</MyButton>
))}
</ScrollView>
);
}

function Gallery() {
return (
<View style={[styles.container, styles.gap, styles.marginBottom]}>
<MyButton style={styles.fullWidthButton} />
<View style={[styles.row, styles.gap]}>
<MyButton style={styles.leftButton} />
<MyButton style={styles.rightButton} />
</View>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
marginBottom: {
marginBottom: 20,
},
paddedContainer: {
padding: 16,
},
heading: {
fontSize: 40,
fontWeight: 'bold',
marginBottom: 24,
color: 'black',
},
gap: {
gap: 10,
},
listItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 20,
backgroundColor: '#232736',
marginVertical: 4,
borderRadius: 20,
marginBottom: 8,
},
listItemLabel: {
fontSize: 20,
flex: 1,
color: 'white',
marginLeft: 20,
},
listItemIcon: {
fontSize: 32,
},
row: {
flexDirection: 'row',
},
avatars: {
width: 90,
height: 90,
borderWidth: 2,
borderColor: '#001A72',
borderTopLeftRadius: 30,
borderTopRightRadius: 5,
borderBottomLeftRadius: 5,
borderBottomRightRadius: 30,
marginHorizontal: 4,
alignItems: 'center',
justifyContent: 'center',
},
avatarLabel: {
color: '#F8F9FF',
fontSize: 24,
fontWeight: 'bold',
},
fullWidthButton: {
width: '100%',
height: 160,
backgroundColor: '#FF6259',
borderTopRightRadius: 30,
borderTopLeftRadius: 30,
borderWidth: 1,
},
leftButton: {
flex: 1,
height: 160,
backgroundColor: '#FFD61E',
borderBottomLeftRadius: 30,
borderWidth: 5,
},
rightButton: {
flex: 1,
backgroundColor: '#782AEB',
height: 160,
borderBottomRightRadius: 30,
borderWidth: 8,
},
});
25 changes: 17 additions & 8 deletions src/components/GestureButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
StyleSheet,
StyleProp,
ViewStyle,
View,
} from 'react-native';

import createNativeWrapper from '../handlers/createNativeWrapper';
Expand All @@ -20,6 +21,7 @@ import {
NativeViewGestureHandlerPayload,
NativeViewGestureHandlerProps,
} from '../handlers/NativeViewGestureHandler';
import { splitStyleProp } from './splitStyleProp';

export interface RawButtonProps extends NativeViewGestureHandlerProps {
/**
Expand Down Expand Up @@ -63,6 +65,7 @@ export interface RawButtonProps extends NativeViewGestureHandlerProps {
* Set this to true if you don't want the system to play sound when the button is pressed.
*/
touchSoundDisabled?: boolean;
style?: StyleProp<ViewStyle>;
}

export interface BaseButtonProps extends RawButtonProps {
Expand All @@ -84,7 +87,6 @@ export interface BaseButtonProps extends RawButtonProps {
* method.
*/
onActiveStateChange?: (active: boolean) => void;
style?: StyleProp<ViewStyle>;
testID?: string;

/**
Expand Down Expand Up @@ -218,15 +220,22 @@ export class BaseButton extends React.Component<BaseButtonProps> {
};

render() {
const { rippleColor, ...rest } = this.props;
const { rippleColor, style, ...rest } = this.props;

const { outerStyles, innerStyles, restStyles } = splitStyleProp(style);

return (
<RawButton
rippleColor={processColor(rippleColor)}
{...rest}
onGestureEvent={this.onGestureEvent}
onHandlerStateChange={this.onHandlerStateChange}
/>
<View style={outerStyles}>
<View style={innerStyles}>
<RawButton
rippleColor={processColor(rippleColor)}
style={restStyles}
{...rest}
onGestureEvent={this.onGestureEvent}
onHandlerStateChange={this.onHandlerStateChange}
/>
</View>
</View>
);
}
}
Expand Down
169 changes: 169 additions & 0 deletions src/components/splitStyleProp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { StyleProp, StyleSheet, ViewStyle } from 'react-native';

const STYLE_GROUPS = {
borderRadiiStyles: {
borderRadius: true,
borderTopLeftRadius: true,
borderTopRightRadius: true,
borderBottomLeftRadius: true,
borderBottomRightRadius: true,
} as const,
outerStyles: {
borderColor: true,
borderWidth: true,
margin: true,
marginBottom: true,
marginEnd: true,
marginHorizontal: true,
marginLeft: true,
marginRight: true,
marginStart: true,
marginTop: true,
marginVertical: true,
width: true,
height: true,
} as const,
innerStyles: {
alignSelf: true,
display: true,
flexBasis: true,
flexGrow: true,
flexShrink: true,
maxHeight: true,
maxWidth: true,
minHeight: true,
minWidth: true,
zIndex: true,
} as const,
applyToAllStyles: {
flex: true,
position: true,
left: true,
right: true,
top: true,
bottom: true,
start: true,
end: true,
} as const,
} as const;

type BorderRadiiKey = keyof typeof STYLE_GROUPS.borderRadiiStyles;
type OuterKey = keyof typeof STYLE_GROUPS.outerStyles;
type InnerKey = keyof typeof STYLE_GROUPS.innerStyles;
type ApplyToAllKey = keyof typeof STYLE_GROUPS.applyToAllStyles;

type BorderRadiiStyles = Pick<ViewStyle, BorderRadiiKey>;
type OuterStyles = Pick<ViewStyle, OuterKey>;
type InnerStyles = Pick<ViewStyle, InnerKey>;
type ApplyToAllStyles = Pick<ViewStyle, ApplyToAllKey>;
type RestStyles = Omit<
ViewStyle,
BorderRadiiKey | OuterKey | InnerKey | ApplyToAllKey
>;

type GroupedStyles = {
borderRadiiStyles: BorderRadiiStyles;
outerStyles: OuterStyles;
innerStyles: InnerStyles;
applyToAllStyles: ApplyToAllStyles;
restStyles: RestStyles;
};

const groupByStyle = (styles: ViewStyle): GroupedStyles => {
const borderRadiiStyles = {} as Record<string, unknown>;
const outerStyles = {} as Record<string, unknown>;
const innerStyles = {} as Record<string, unknown>;
const applyToAllStyles = {} as Record<string, unknown>;
const restStyles = {} as Record<string, unknown>;

let key: keyof ViewStyle;

for (key in styles) {
if (key in STYLE_GROUPS.borderRadiiStyles) {
borderRadiiStyles[key] = styles[key];
} else if (key in STYLE_GROUPS.outerStyles) {
outerStyles[key] = styles[key];
} else if (key in STYLE_GROUPS.innerStyles) {
innerStyles[key] = styles[key];
} else if (key in STYLE_GROUPS.applyToAllStyles) {
applyToAllStyles[key] = styles[key];
} else {
restStyles[key] = styles[key];
}
}

return {
borderRadiiStyles,
outerStyles,
innerStyles,
applyToAllStyles,
restStyles,
};
};

// if borderWidth was specified it will adjust the border radii
// to remain the same curvature for both inner and outer views
// https://twitter.com/lilykonings/status/1567317037126680576
const shrinkBorderRadiiByBorderWidth = (
borderRadiiStyles: BorderRadiiStyles,
borderWidth: number
) => {
const newBorderRadiiStyles = { ...borderRadiiStyles };

let borderRadiusType: BorderRadiiKey;

for (borderRadiusType in newBorderRadiiStyles) {
newBorderRadiiStyles[borderRadiusType] =
(newBorderRadiiStyles[borderRadiusType] as number) - borderWidth;
}

return newBorderRadiiStyles;
};

export function splitStyleProp<T extends ViewStyle>(
style?: StyleProp<T>
): {
outerStyles: T;
innerStyles: T;
restStyles: T;
} {
const resolvedStyle = StyleSheet.flatten((style ?? {}) as ViewStyle);

let outerStyles = {} as T;
let innerStyles = { overflow: 'hidden', flexGrow: 1 } as T;
let restStyles = { flexGrow: 1 } as T;

const styleGroups = groupByStyle(resolvedStyle);

outerStyles = {
...outerStyles,
...styleGroups.borderRadiiStyles,
...styleGroups.applyToAllStyles,
...styleGroups.outerStyles,
};
innerStyles = {
...innerStyles,
...styleGroups.applyToAllStyles,
...styleGroups.innerStyles,
};
restStyles = {
...restStyles,
...styleGroups.restStyles,
...styleGroups.applyToAllStyles,
};

// if borderWidth was specified it adjusts border radii
// to remain the same curvature for both inner and outer views
if (styleGroups.outerStyles.borderWidth != null) {
const { borderWidth } = styleGroups.outerStyles;

const innerBorderRadiiStyles = shrinkBorderRadiiByBorderWidth(
{ ...styleGroups.borderRadiiStyles },
borderWidth
);

innerStyles = { ...innerStyles, ...innerBorderRadiiStyles };
}

return { outerStyles, innerStyles, restStyles };
}
j-piasecki marked this conversation as resolved.
Show resolved Hide resolved
Loading