| 1 | import { useEffect, useState } from "react"; |
| 2 | import { I18nManager, Platform, Pressable, View } from "react-native"; |
| 3 | import Animated, { |
| 4 | Easing, |
| 5 | ReduceMotion, |
| 6 | useAnimatedStyle, |
| 7 | useSharedValue, |
| 8 | withTiming, |
| 9 | } from "react-native-reanimated"; |
| 10 | import { AppText as Text } from "./AppText"; |
| 11 | import { cn } from "../lib/cn"; |
| 12 | import { MaterialSegmentedControl } from "./MaterialSegmentedControl"; |
| 13 | import type { SegmentedControlProps } from "./SegmentedControl.types"; |
| 14 | |
| 15 | export type { SegmentedControlProps } from "./SegmentedControl.types"; |
| 16 | |
| 17 | export function SegmentedControl<Value extends number | string>( |
| 18 | props: SegmentedControlProps<Value>, |
| 19 | ) { |
| 20 | const compact = props.size === "compact"; |
| 21 | const [width, setWidth] = useState(0); |
| 22 | const index = Math.max( |
| 23 | 0, |
| 24 | props.options.findIndex((option) => option.value === props.selected), |
| 25 | ); |
| 26 | const position = useSharedValue(index); |
| 27 | useEffect(() => { |
| 28 | position.set( |
| 29 | withTiming(index, { |
| 30 | duration: 200, |
| 31 | easing: Easing.out(Easing.cubic), |
| 32 | reduceMotion: ReduceMotion.System, |
| 33 | }), |
| 34 | ); |
| 35 | }, [index, position]); |
| 36 | const indicatorStyle = useAnimatedStyle(() => ({ |
| 37 | transform: [ |
| 38 | { |
| 39 | translateX: |
| 40 | ((position.value * width) / props.options.length) * (I18nManager.isRTL ? -1 : 1), |
| 41 | }, |
| 42 | ], |
| 43 | })); |
| 44 | if (Platform.OS === "android") { |
| 45 | return <MaterialSegmentedControl {...props} />; |
| 46 | } |
| 47 | return ( |
| 48 | <View |
| 49 | accessible={false} |
| 50 | onLayout={(event) => setWidth(event.nativeEvent.layout.width)} |
| 51 | className={cn( |
| 52 | "flex-row overflow-hidden", |
| 53 | "rounded-full border-continuous bg-card", |
| 54 | props.className, |
| 55 | )} |
| 56 | > |
| 57 | <Animated.View |
| 58 | pointerEvents="none" |
| 59 | className="absolute inset-y-0 rounded-full bg-secondary" |
| 60 | style={[{ width: `${100 / props.options.length}%`, start: 0 }, indicatorStyle]} |
| 61 | /> |
| 62 | {props.options.map((option) => { |
| 63 | const active = option.value === props.selected; |
| 64 | return ( |
| 65 | <Pressable |
| 66 | key={String(option.value)} |
| 67 | accessibilityRole={Platform.OS === "ios" ? "button" : (props.role ?? "button")} |
| 68 | accessibilityLabel={option.accessibilityLabel ?? option.label} |
| 69 | accessibilityState={{ selected: active }} |
| 70 | onPress={() => props.onSelect(option.value)} |
| 71 | className={cn( |
| 72 | "flex-1 items-center justify-center rounded-full", |
| 73 | compact ? "h-9" : "h-11", |
| 74 | )} |
| 75 | > |
| 76 | <Text |
| 77 | className={cn( |
| 78 | compact ? "text-xs" : "text-sm", |
| 79 | active ? "font-t3-medium text-secondary-foreground" : "text-foreground-muted", |
| 80 | )} |
| 81 | > |
| 82 | {option.label} |
| 83 | </Text> |
| 84 | </Pressable> |
| 85 | ); |
| 86 | })} |
| 87 | </View> |
| 88 | ); |
| 89 | } |