Skip to content

Commit 3fe7a10

Browse files
committed
feat: Add PillSwitch basic component with examples
1 parent ec64f72 commit 3fe7a10

File tree

6 files changed

+677
-9
lines changed

6 files changed

+677
-9
lines changed

example/src/App.tsx

Lines changed: 149 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,163 @@
1-
import { Text, View, StyleSheet } from 'react-native';
2-
import { multiply } from 'react-native-multiswitch-controller';
3-
4-
const result = multiply(3, 7);
1+
import { View, StyleSheet, Text } from 'react-native';
2+
import {
3+
PillSwitch,
4+
useControlListState,
5+
} from 'react-native-multiswitch-controller';
56

67
export default function App() {
8+
const controlListState1 = useControlListState({
9+
options: [
10+
{ value: 'First', label: 'First' },
11+
{ value: 'Second', label: 'Second' },
12+
{ value: 'Third', label: 'Third' },
13+
{ value: 'Fourth', label: 'Fourth' },
14+
{ value: 'Fifth', label: 'Fifth' },
15+
{ value: 'Sixth', label: 'Sixth' },
16+
{ value: 'Seventh', label: 'Seventh' },
17+
{ value: 'Eighth', label: 'Eighth' },
18+
{ value: 'Ninth', label: 'Ninth' },
19+
{ value: 'Tenth', label: 'Tenth' },
20+
{ value: 'Eleventh', label: 'Eleventh' },
21+
{ value: 'Twelfth', label: 'Twelfth' },
22+
{ value: 'Thirteenth', label: 'Thirteenth' },
23+
{ value: 'Fourteenth', label: 'Fourteenth' },
24+
{ value: 'Fifteenth', label: 'Fifteenth' },
25+
{ value: 'Sixteenth', label: 'Sixteenth' },
26+
],
27+
defaultOption: 'First',
28+
variant: 'segmentedControl',
29+
});
30+
const controlListState2 = useControlListState({
31+
options: [
32+
{ value: 'First', label: 'First' },
33+
{ value: 'Second', label: 'Second' },
34+
],
35+
defaultOption: 'First',
36+
variant: 'segmentedControl',
37+
});
38+
const controlListState3 = useControlListState({
39+
options: [
40+
{ value: 'First', label: 'First' },
41+
{ value: 'Second', label: 'Second' },
42+
],
43+
defaultOption: 'Second',
44+
variant: 'segmentedControl',
45+
});
46+
const controlListState4 = useControlListState({
47+
options: [
48+
{ value: 'First', label: 'First is a very long label' },
49+
{ value: 'Second', label: 'Second is short' },
50+
],
51+
defaultOption: 'First',
52+
variant: 'segmentedControl',
53+
});
54+
55+
const isTestMode = false;
56+
757
return (
858
<View style={styles.container}>
9-
<Text>Result: {result}</Text>
59+
<View style={styles.exampleContainer}>
60+
<Text style={styles.title}>Align to left, center or right</Text>
61+
<PillSwitch
62+
controlListState={controlListState2}
63+
align="left"
64+
inactiveBackgroundColor="rgb(21, 87, 21)"
65+
activeBackgroundColor="rgb(60, 180, 20)"
66+
inactiveTextColor="rgb(208, 249, 205)"
67+
/>
68+
<PillSwitch
69+
controlListState={controlListState2}
70+
align="center"
71+
inactiveBackgroundColor="rgba(220, 38, 38, 0.08)"
72+
activeBackgroundColor="rgb(185, 28, 28)"
73+
inactiveTextColor="rgb(185, 28, 28)"
74+
/>
75+
<PillSwitch
76+
controlListState={controlListState2}
77+
align="right"
78+
inactiveBackgroundColor="rgba(205, 197, 40, 0.08)"
79+
activeBackgroundColor="rgb(180, 170, 20)"
80+
inactiveTextColor="rgb(180, 170, 20)"
81+
/>
82+
{isTestMode && (
83+
<Text style={styles.selectedText}>
84+
Selected: {controlListState2.activeOption}
85+
</Text>
86+
)}
87+
</View>
88+
89+
<View style={styles.exampleContainer}>
90+
<Text style={styles.title}>Set initial value</Text>
91+
<PillSwitch
92+
controlListState={controlListState3}
93+
inactiveBackgroundColor="rgba(59, 130, 246, 0.08)"
94+
activeBackgroundColor="rgb(37, 99, 235)"
95+
inactiveTextColor="rgb(37, 99, 235)"
96+
activeTextColor="rgb(253, 230, 138)"
97+
/>
98+
{isTestMode && (
99+
<Text style={styles.selectedText}>
100+
Selected: {controlListState3.activeOption}
101+
</Text>
102+
)}
103+
</View>
104+
105+
<View style={styles.exampleContainer}>
106+
<Text style={styles.title}>Use different width for labels</Text>
107+
<PillSwitch
108+
controlListState={controlListState4}
109+
containerHeight={48}
110+
itemHeight={36}
111+
inactiveBackgroundColor="rgba(30, 64, 175, 0.08)"
112+
activeBackgroundColor="rgb(30, 64, 175)"
113+
inactiveTextColor="rgb(30, 64, 175)"
114+
/>
115+
{isTestMode && (
116+
<Text style={styles.selectedText}>
117+
Selected: {controlListState4.activeOption}
118+
</Text>
119+
)}
120+
</View>
121+
122+
<View style={styles.exampleContainer}>
123+
<Text style={styles.title}>
124+
Scrollable if there is not enough width
125+
</Text>
126+
<PillSwitch
127+
controlListState={controlListState1}
128+
customItemStyle={{}}
129+
containerHeight={54}
130+
itemHeight={48}
131+
/>
132+
{isTestMode && (
133+
<Text style={styles.selectedText}>
134+
Selected: {controlListState1.activeOption}
135+
</Text>
136+
)}
137+
</View>
10138
</View>
11139
);
12140
}
13141

14142
const styles = StyleSheet.create({
15143
container: {
16144
flex: 1,
17-
alignItems: 'center',
18145
justifyContent: 'center',
146+
gap: 16,
147+
padding: 10,
148+
},
149+
title: {
150+
fontSize: 18,
151+
padding: 10,
152+
},
153+
selectedText: {
154+
fontSize: 16,
155+
padding: 4,
156+
alignSelf: 'center',
157+
},
158+
bigContainer: {
159+
width: '100%',
160+
marginBottom: 20,
19161
},
162+
exampleContainer: {},
20163
});

src/PillSwitch.tsx

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { useCallback, useMemo } from 'react';
2+
import { StyleSheet, View, type ViewStyle } from 'react-native';
3+
import Animated from 'react-native-reanimated';
4+
5+
import PillSwitchItem from './PillSwitchItem';
6+
import type { ControlOption } from './types';
7+
import type { ControlListState } from './useControlListState';
8+
9+
type AlignmentOption = 'left' | 'right' | 'center';
10+
11+
export type PillSwitchProps<TValue> = {
12+
controlListState: ControlListState<TValue>;
13+
align?: AlignmentOption;
14+
onPressCallback?: (value: TValue) => void;
15+
16+
// Styling props
17+
customItemStyle?: ViewStyle;
18+
containerHeight?: number;
19+
itemHeight?: number;
20+
21+
inactiveBackgroundColor?: string;
22+
activeBackgroundColor?: string;
23+
inactiveTextColor?: string;
24+
activeTextColor?: string;
25+
};
26+
27+
function PillSwitch<TValue>(props: PillSwitchProps<TValue>) {
28+
const {
29+
controlListState,
30+
align = 'center',
31+
onPressCallback,
32+
customItemStyle,
33+
containerHeight = 50,
34+
itemHeight = 48,
35+
36+
inactiveBackgroundColor = 'rgb(232, 221, 250)',
37+
activeBackgroundColor = 'rgb(124, 58, 237)',
38+
inactiveTextColor = 'rgb(124, 58, 237)',
39+
activeTextColor = '#fff',
40+
} = props;
41+
42+
const {
43+
options,
44+
activeOption,
45+
onLayoutOptionItem,
46+
onAnimationFinish,
47+
animatedActiveOptionIndex,
48+
animatedActiveOptionStyle,
49+
scrollHandler,
50+
controlListRef,
51+
} = controlListState;
52+
53+
const containerPadding = (containerHeight - itemHeight) / 2;
54+
55+
const defaultItemStyle: ViewStyle = useMemo(
56+
() => ({
57+
height: itemHeight || 48,
58+
paddingVertical: 8,
59+
paddingHorizontal: 12,
60+
}),
61+
[itemHeight]
62+
);
63+
64+
const renderItem = useCallback(
65+
({ item, index }: { item: ControlOption<TValue>; index: number }) => (
66+
<PillSwitchItem
67+
item={item}
68+
isActive={activeOption === item.value}
69+
index={index}
70+
onLayout={onLayoutOptionItem}
71+
onChange={() => {
72+
onAnimationFinish(item.value);
73+
onPressCallback?.(item.value);
74+
}}
75+
animatedActiveOptionIndex={animatedActiveOptionIndex}
76+
textColorActive={activeTextColor}
77+
textColorInactive={inactiveTextColor}
78+
itemContainerStyle={{ ...defaultItemStyle, ...customItemStyle }}
79+
/>
80+
),
81+
[
82+
onLayoutOptionItem,
83+
activeOption,
84+
onAnimationFinish,
85+
animatedActiveOptionIndex,
86+
onPressCallback,
87+
defaultItemStyle,
88+
customItemStyle,
89+
activeTextColor,
90+
inactiveTextColor,
91+
]
92+
);
93+
94+
const containerStyles: ViewStyle = useMemo(() => {
95+
return {
96+
height: containerHeight,
97+
padding: containerPadding,
98+
marginRight: align !== 'right' ? 'auto' : 0,
99+
marginLeft: align !== 'left' ? 'auto' : 0,
100+
backgroundColor: inactiveBackgroundColor,
101+
};
102+
}, [containerHeight, containerPadding, align, inactiveBackgroundColor]);
103+
104+
const defaultActiveOptionStyles: ViewStyle = useMemo(() => {
105+
return {
106+
height: itemHeight,
107+
top: containerPadding,
108+
left: containerPadding,
109+
backgroundColor: activeBackgroundColor,
110+
};
111+
}, [containerPadding, activeBackgroundColor, itemHeight]);
112+
113+
if (!options.length) return null;
114+
115+
return (
116+
<View style={[styles.container, containerStyles]}>
117+
<Animated.View
118+
style={[
119+
styles.activeOption,
120+
defaultActiveOptionStyles,
121+
animatedActiveOptionStyle,
122+
]}
123+
/>
124+
<Animated.FlatList
125+
ref={controlListRef}
126+
onScroll={scrollHandler}
127+
scrollEventThrottle={12}
128+
accessibilityRole="tablist"
129+
data={options}
130+
keyExtractor={(item) => String(item.value)}
131+
renderItem={renderItem}
132+
showsHorizontalScrollIndicator={false}
133+
horizontal
134+
overScrollMode="never"
135+
bounces={false}
136+
/>
137+
</View>
138+
);
139+
}
140+
141+
const styles = StyleSheet.create({
142+
container: {
143+
borderRadius: 999,
144+
overflow: 'hidden',
145+
},
146+
activeOption: {
147+
borderRadius: 999,
148+
position: 'absolute',
149+
},
150+
});
151+
152+
export default PillSwitch;

0 commit comments

Comments
 (0)