Open History to see the last commit
1import { useState } from "react";
2import { I18nManager, Platform, Pressable, View } from "react-native";
3import Animated, {
4 useAnimatedStyle,
5 useDerivedValue,
6 useReducedMotion,
7 withTiming,
8} from "react-native-reanimated";
9
10import { segmentedControlColors } from "./segmentedControlColors";
11import { AppText } from "./AppText";
12import { useControlTheme } from "./useControlTheme";
13import type { SegmentedControlProps } from "./SegmentedControl.types";
14
15export type { SegmentedControlProps } from "./SegmentedControl.types";
16
17export function SegmentedControl<Value extends string | number>(
18 props: SegmentedControlProps<Value>,
19) {
20 const { colorScheme, colors, themeId } = useControlTheme();
21 const [layout, setLayout] = useState({ width: 0, height: 0 });
22 const { width } = layout;
23 const reduceMotion = useReducedMotion();
24 const selectedIndex = props.options.findIndex((option) => option.value === props.selected);
25 const segmentWidth = props.options.length ? Math.max(0, width - 6) / props.options.length : 0;
26 const direction = I18nManager.isRTL ? -1 : 1;
27 // The index animates, not the offset, so a resize moves the selection without sliding it.
28 const selectedPosition = useDerivedValue(() =>
29 withTiming(selectedIndex, { duration: reduceMotion ? 0 : 160 }),
30 );
31 const selectionStyle = useAnimatedStyle(() => ({
32 width: segmentWidth,
33 transform: [{ translateX: selectedPosition.value * segmentWidth * direction }],
34 }));
35 const { trackColor, selectedColor, selectedTextColor, mutedColor } = segmentedControlColors(
36 colors,
37 colorScheme,
38 props.variant,
39 Platform.OS,
40 themeId,
41 );
42 const minHeight = props.size === "compact" ? 32 : Platform.OS === "android" ? 40 : 36;
43
44 return (
45 <View collapsable={false} className={props.className}>
46 <View
47 onLayout={(event) => setLayout(event.nativeEvent.layout)}
48 className="flex-row rounded-full p-[3px]"
49 style={{
50 minHeight,
51 backgroundColor: trackColor,
52 }}
53 >
54 {selectedIndex >= 0 && width > 0 ? (
55 <Animated.View
56 pointerEvents="none"
57 className="absolute bottom-[3px] start-[3px] top-[3px] rounded-full"
58 style={[
59 {
60 backgroundColor: selectedColor,
61 },
62 selectionStyle,
63 ]}
64 />
65 ) : null}
66 {props.options.map((option) => {
67 const selected = option.value === props.selected;
68 return (
69 <Pressable
70 key={`${typeof option.value}:${option.value}`}
71 accessibilityRole={props.role ?? "button"}
72 accessibilityLabel={option.accessibilityLabel ?? option.label}
73 accessibilityState={{ selected }}
74 onPress={() => props.onSelect(option.value)}
75 className="flex-1 items-center justify-center rounded-full px-2 py-1 active:opacity-70"
76 style={{ minHeight: minHeight - 6 }}
77 >
78 <AppText
79 textRole="footnote"
80 className={selected ? "font-t3-bold" : "font-t3-medium"}
81 style={{
82 color: selected ? selectedTextColor : mutedColor,
83 }}
84 >
85 {option.label}
86 </AppText>
87 </Pressable>
88 );
89 })}
90 </View>
91 </View>
92 );
93}