Files
fiberops-mobile/components/SlideToConfirm.tsx

124 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback } from 'react';
import { StyleSheet, Text, View, ViewStyle } from 'react-native';
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
interface Props {
onConfirm: () => void;
label?: string;
color?: string;
threshold?: number;
disabled?: boolean;
style?: ViewStyle;
}
const TRACK_HEIGHT = 60;
const HANDLE_SIZE = 48;
const TRACK_PADDING = 5;
export function SlideToConfirm({
onConfirm,
label = 'Slide to confirm',
color = '#059669',
threshold = 0.82,
disabled = false,
style,
}: Props) {
const translateX = useSharedValue(0);
const maxX = useSharedValue(200); // updated on layout
const triggerConfirm = useCallback(() => { onConfirm(); }, [onConfirm]);
const pan = Gesture.Pan()
.enabled(!disabled)
.minDistance(0)
.activeOffsetX([-5, 5])
.onUpdate((e) => {
'worklet';
translateX.value = Math.max(0, Math.min(e.translationX, maxX.value));
})
.onEnd(() => {
'worklet';
if (translateX.value >= maxX.value * threshold) {
translateX.value = withSpring(maxX.value, { damping: 20, stiffness: 200, mass: 0.8 });
runOnJS(triggerConfirm)();
} else {
translateX.value = withSpring(0, { damping: 20, stiffness: 200, mass: 0.8 });
}
});
const animatedHandle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
const animatedFill = useAnimatedStyle(() => ({
width: translateX.value + HANDLE_SIZE + TRACK_PADDING,
}));
const animatedLabel = useAnimatedStyle(() => ({
opacity: Math.max(0, 1 - (translateX.value / Math.max(maxX.value * 0.5, 1))),
}));
return (
// GestureHandlerRootView inside Modal is REQUIRED on iOS New Arch
// Modal creates a new UIWindow — RNGH's gesture manager must be re-rooted here
<GestureHandlerRootView style={[styles.root, style]}>
<View
style={[styles.track, { borderColor: color + '50' }]}
onLayout={(e) => {
const w = e.nativeEvent.layout.width;
maxX.value = w - HANDLE_SIZE - TRACK_PADDING * 2;
}}
>
{/* Colored fill that grows behind the handle */}
<Animated.View style={[styles.fill, { backgroundColor: color + '28' }, animatedFill]} />
{/* Label fades as handle moves right */}
<Animated.Text
style={[styles.label, { paddingLeft: HANDLE_SIZE + TRACK_PADDING * 3 }, animatedLabel]}
numberOfLines={1}
>
{label}
</Animated.Text>
{/* Draggable handle */}
<GestureDetector gesture={pan}>
<Animated.View style={[
styles.handle,
{ backgroundColor: disabled ? '#CBD5E1' : color, width: HANDLE_SIZE, height: HANDLE_SIZE, borderRadius: HANDLE_SIZE / 2, top: (TRACK_HEIGHT - HANDLE_SIZE) / 2, left: TRACK_PADDING },
animatedHandle,
]}>
<Text style={styles.arrow}></Text>
</Animated.View>
</GestureDetector>
</View>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
root: { width: '100%' },
track: {
height: TRACK_HEIGHT, borderRadius: TRACK_HEIGHT / 2,
backgroundColor: '#F1F5F9', borderWidth: 1.5,
justifyContent: 'center', alignItems: 'center',
overflow: 'hidden', position: 'relative',
},
fill: {
position: 'absolute', left: 0, top: 0,
height: TRACK_HEIGHT, zIndex: 1,
},
label: {
position: 'absolute', zIndex: 2,
fontSize: 14, fontWeight: '700', color: '#64748B',
textAlign: 'center', width: '100%', paddingRight: 16,
},
handle: {
position: 'absolute', zIndex: 3,
justifyContent: 'center', alignItems: 'center',
shadowColor: '#000', shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.18, shadowRadius: 4, elevation: 4,
},
arrow: { fontSize: 26, color: '#fff', fontWeight: '800', marginLeft: 2 },
});