template_0205
This commit is contained in:
256
RN_TEMPLATE/app/components/Toggle/Switch.tsx
Normal file
256
RN_TEMPLATE/app/components/Toggle/Switch.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
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<ToggleProps<SwitchInputProps>, "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<ViewStyle, "width" | "height"> & { width?: number; height?: number }
|
||||
}
|
||||
|
||||
interface SwitchInputProps extends BaseToggleInputProps<SwitchToggleProps> {
|
||||
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) => (
|
||||
<SwitchInput {...toggleProps} accessibilityMode={accessibilityMode} />
|
||||
),
|
||||
[accessibilityMode],
|
||||
)
|
||||
return <Toggle accessibilityRole="switch" {...rest} ToggleInput={switchInput} />
|
||||
}
|
||||
|
||||
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 (
|
||||
<View style={[$inputOuter, { backgroundColor: offBackgroundColor }, $outerStyleOverride]}>
|
||||
<Animated.View
|
||||
style={[
|
||||
$themedSwitchInner,
|
||||
{ backgroundColor: onBackgroundColor },
|
||||
$innerStyleOverride,
|
||||
{ opacity: opacity.current },
|
||||
]}
|
||||
/>
|
||||
|
||||
<SwitchAccessibilityLabel {...props} role="on" />
|
||||
<SwitchAccessibilityLabel {...props} role="off" />
|
||||
|
||||
<Animated.View
|
||||
style={[
|
||||
$switchDetail,
|
||||
$detailStyleOverride,
|
||||
{ transform: [{ translateX: $animatedSwitchKnob }] },
|
||||
{ width: knobWidth, height: knobHeight },
|
||||
{ backgroundColor: knobBackgroundColor },
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @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<ViewStyle> = [
|
||||
$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 (
|
||||
<View style={$switchAccessibilityStyle}>
|
||||
{accessibilityMode === "text" && shouldLabelBeVisible && (
|
||||
<View
|
||||
style={[
|
||||
role === "on" && $switchAccessibilityLine,
|
||||
role === "on" && { backgroundColor: color },
|
||||
role === "off" && $switchAccessibilityCircle,
|
||||
role === "off" && { borderColor: color },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{accessibilityMode === "icon" && shouldLabelBeVisible && (
|
||||
<Icon icon={role === "off" ? "hidden" : "view"} size={14} color={color} />
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const $inputOuter: StyleProp<ViewStyle> = [
|
||||
$inputOuterBase,
|
||||
{ height: s(32), width: s(56), borderRadius: s(16), borderWidth: 0 },
|
||||
]
|
||||
|
||||
const $switchInner: ThemedStyle<ViewStyle> = ({ 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),
|
||||
}
|
||||
Reference in New Issue
Block a user