feat : Switch
This commit is contained in:
151
src/components/form/Switch.tsx
Normal file
151
src/components/form/Switch.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import {
|
||||
Animated,
|
||||
Easing,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
View,
|
||||
type StyleProp,
|
||||
type ViewStyle,
|
||||
} from 'react-native';
|
||||
|
||||
import { colors } from '@/theme/colors';
|
||||
|
||||
export type SwitchSize = 'md' | 'lg';
|
||||
|
||||
export type SwitchProps = {
|
||||
value: boolean;
|
||||
onValueChange?: (value: boolean) => void;
|
||||
disabled?: boolean;
|
||||
testID?: string;
|
||||
accessibilityLabel?: string;
|
||||
size?: SwitchSize;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
const SIZE_BY_VARIANT: Record<
|
||||
SwitchSize,
|
||||
{ width: number; height: number; thumb: number; padding: number }
|
||||
> = {
|
||||
md: { width: 56, height: 34, thumb: 26, padding: 4 },
|
||||
lg: { width: 64, height: 38, thumb: 30, padding: 4 },
|
||||
};
|
||||
|
||||
function Switch({
|
||||
size = 'md',
|
||||
value,
|
||||
disabled,
|
||||
onValueChange,
|
||||
testID,
|
||||
accessibilityLabel,
|
||||
containerStyle,
|
||||
style,
|
||||
}: SwitchProps) {
|
||||
const metrics = SIZE_BY_VARIANT[size];
|
||||
const travel = metrics.width - metrics.padding * 2 - metrics.thumb;
|
||||
const progress = useRef(new Animated.Value(value ? 1 : 0)).current;
|
||||
|
||||
useEffect(() => {
|
||||
Animated.timing(progress, {
|
||||
toValue: value ? 1 : 0,
|
||||
duration: 180,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}, [progress, value]);
|
||||
|
||||
const thumbTranslateX = progress.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, travel],
|
||||
});
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
testID={testID}
|
||||
accessibilityRole="switch"
|
||||
accessibilityState={{ checked: value, disabled }}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
disabled={disabled}
|
||||
onPress={() => onValueChange?.(!value)}
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
width: metrics.width,
|
||||
height: metrics.height,
|
||||
borderRadius: metrics.height / 2,
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
},
|
||||
containerStyle,
|
||||
]}
|
||||
>
|
||||
{value ? (
|
||||
<LinearGradient
|
||||
colors={[colors.secondary, colors.primary]}
|
||||
start={{ x: 0, y: 0.5 }}
|
||||
end={{ x: 1, y: 0.5 }}
|
||||
style={[
|
||||
styles.trackFill,
|
||||
{ borderRadius: metrics.height / 2, borderColor: 'rgba(255,255,255,0.18)' },
|
||||
]}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
style={[
|
||||
styles.trackFill,
|
||||
styles.trackOff,
|
||||
{ borderRadius: metrics.height / 2 },
|
||||
]}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.thumb,
|
||||
{
|
||||
width: metrics.thumb,
|
||||
height: metrics.thumb,
|
||||
borderRadius: metrics.thumb / 2,
|
||||
top: metrics.padding,
|
||||
left: metrics.padding,
|
||||
transform: [{ translateX: thumbTranslateX }],
|
||||
backgroundColor: value ? '#FFFFFF' : colors.textStrong,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
trackFill: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
borderWidth: 1,
|
||||
},
|
||||
trackOff: {
|
||||
backgroundColor: colors.surfaceElevated,
|
||||
borderColor: colors.inputBorder,
|
||||
},
|
||||
thumb: {
|
||||
position: 'absolute',
|
||||
shadowColor: '#000000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
});
|
||||
|
||||
export default Switch;
|
||||
@@ -2,11 +2,13 @@ export { default as FormField } from './FormField';
|
||||
export { default as Input } from './Input';
|
||||
export { default as PasswordInput } from './PasswordInput';
|
||||
export { default as SearchInput } from './SearchInput';
|
||||
export { default as Switch } from './Switch';
|
||||
export { default as TextArea } from './TextArea';
|
||||
|
||||
export type { FieldCounter, FormFieldProps } from './FormField';
|
||||
export type { InputProps, InputSize, InputState } from './Input';
|
||||
export type { PasswordInputProps, PasswordStrength } from './PasswordInput';
|
||||
export type { SearchInputProps } from './SearchInput';
|
||||
export type { SwitchProps, SwitchSize } from './Switch';
|
||||
export type { TextAreaProps } from './TextArea';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user