import { useEffect, useMemo, useRef, useCallback } from "react" import { Animated, Platform, StyleProp, View, ViewStyle } from "react-native" import { Icon } from "@/components/Icon" import { isRTL } from "@/i18n" import { useAppTheme } from "@/theme/context" import { $styles } from "@/theme/styles" import type { ThemedStyle } from "@/theme/types" import { s } from "@/utils/responsive" import { $inputOuterBase, BaseToggleInputProps, Toggle, ToggleProps } from "./Toggle" export interface SwitchToggleProps extends Omit, "ToggleInput"> { /** * Switch-only prop that adds a text/icon label for on/off states. */ accessibilityMode?: "text" | "icon" /** * Optional style prop that affects the knob View. * Note: `width` and `height` rules should be points (numbers), not percentages. */ inputDetailStyle?: Omit & { width?: number; height?: number } } interface SwitchInputProps extends BaseToggleInputProps { accessibilityMode?: SwitchToggleProps["accessibilityMode"] } /** * @param {SwitchToggleProps} props - The props for the `Switch` component. * @see [Documentation and Examples]{@link https://docs.infinite.red/ignite-cli/boilerplate/app/components/Switch} * @returns {JSX.Element} The rendered `Switch` component. */ export function Switch(props: SwitchToggleProps) { const { accessibilityMode, ...rest } = props const switchInput = useCallback( (toggleProps: SwitchInputProps) => ( ), [accessibilityMode], ) return } function SwitchInput(props: SwitchInputProps) { const { on, status, disabled, outerStyle: $outerStyleOverride, innerStyle: $innerStyleOverride, detailStyle: $detailStyleOverride, } = props const { theme: { colors }, themed, } = useAppTheme() const animate = useRef(new Animated.Value(on ? 1 : 0)) // Initial value is set based on isActive const opacity = useRef(new Animated.Value(0)) useEffect(() => { Animated.timing(animate.current, { toValue: on ? 1 : 0, duration: 300, useNativeDriver: true, // Enable native driver for smoother animations }).start() }, [on]) useEffect(() => { Animated.timing(opacity.current, { toValue: on ? 1 : 0, duration: 300, useNativeDriver: true, }).start() }, [on]) const knobSizeFallback = 2 const knobWidth = [$detailStyleOverride?.width, $switchDetail?.width, knobSizeFallback].find( (v) => typeof v === "number", ) const knobHeight = [$detailStyleOverride?.height, $switchDetail?.height, knobSizeFallback].find( (v) => typeof v === "number", ) const offBackgroundColor = [ disabled && colors.palette.neutral400, status === "error" && colors.errorBackground, colors.palette.neutral300, ].filter(Boolean)[0] const onBackgroundColor = [ disabled && colors.transparent, status === "error" && colors.errorBackground, colors.palette.secondary500, ].filter(Boolean)[0] const knobBackgroundColor = (function () { if (on) { return [ $detailStyleOverride?.backgroundColor, status === "error" && colors.error, disabled && colors.palette.neutral600, colors.palette.neutral100, ].filter(Boolean)[0] } else { return [ $innerStyleOverride?.backgroundColor, disabled && colors.palette.neutral600, status === "error" && colors.error, colors.palette.neutral200, ].filter(Boolean)[0] } })() const rtlAdjustment = isRTL ? -1 : 1 const $themedSwitchInner = useMemo(() => themed([$styles.toggleInner, $switchInner]), [themed]) const offsetLeft = ($innerStyleOverride?.paddingStart || $innerStyleOverride?.paddingLeft || $themedSwitchInner?.paddingStart || $themedSwitchInner?.paddingLeft || 0) as number const offsetRight = ($innerStyleOverride?.paddingEnd || $innerStyleOverride?.paddingRight || $themedSwitchInner?.paddingEnd || $themedSwitchInner?.paddingRight || 0) as number const outputRange = Platform.OS === "web" ? isRTL ? [+(knobWidth || 0) + offsetRight, offsetLeft] : [offsetLeft, +(knobWidth || 0) + offsetRight] : [rtlAdjustment * offsetLeft, rtlAdjustment * (+(knobWidth || 0) + offsetRight)] const $animatedSwitchKnob = animate.current.interpolate({ inputRange: [0, 1], outputRange, }) return ( ) } /** * @param {ToggleInputProps & { role: "on" | "off" }} props - The props for the `SwitchAccessibilityLabel` component. * @returns {JSX.Element} The rendered `SwitchAccessibilityLabel` component. */ function SwitchAccessibilityLabel(props: SwitchInputProps & { role: "on" | "off" }) { const { on, disabled, status, accessibilityMode, role, innerStyle, detailStyle } = props const { theme: { colors }, } = useAppTheme() if (!accessibilityMode) return null const shouldLabelBeVisible = (on && role === "on") || (!on && role === "off") const $switchAccessibilityStyle: StyleProp = [ $switchAccessibility, role === "off" && { end: "5%" }, role === "on" && { left: "5%" }, ] const color = (function () { if (disabled) return colors.palette.neutral600 if (status === "error") return colors.error if (!on) return innerStyle?.backgroundColor || colors.palette.secondary500 return detailStyle?.backgroundColor || colors.palette.neutral100 })() as string return ( {accessibilityMode === "text" && shouldLabelBeVisible && ( )} {accessibilityMode === "icon" && shouldLabelBeVisible && ( )} ) } const $inputOuter: StyleProp = [ $inputOuterBase, { height: s(32), width: s(56), borderRadius: s(16), borderWidth: 0 }, ] const $switchInner: ThemedStyle = ({ colors }) => ({ borderColor: colors.transparent, position: "absolute", paddingStart: 4, paddingEnd: 4, }) const $switchDetail: SwitchToggleProps["inputDetailStyle"] = { borderRadius: s(12), position: "absolute", width: s(24), height: s(24), } const $switchAccessibility: ViewStyle = { width: "40%", justifyContent: "center", alignItems: "center", } const $switchAccessibilityLine: ViewStyle = { width: 2, height: s(12), } const $switchAccessibilityCircle: ViewStyle = { borderWidth: 2, width: s(12), height: s(12), borderRadius: s(6), }