Files
fiberops-mobile/components/SlideToConfirm.tsx

145 lines
4.0 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.
/**
* SlideToConfirm — pure RN, no RNGH, no Reanimated.
* PanResponder on handle + Animated(useNativeDriver:false).
*/
import { useRef } from 'react';
import {
Animated,
PanResponder,
StyleSheet,
Text,
View,
ViewStyle,
} from 'react-native';
interface Props {
onConfirm: () => void;
label?: string;
disabled?: boolean;
style?: ViewStyle;
color?: string;
}
const HANDLE_SIZE = 56;
const THRESHOLD = 0.80;
export function SlideToConfirm({ onConfirm, label = 'Slide to confirm', disabled = false, style, color }: Props) {
const trackWidth = useRef(0);
const confirmed = useRef(false);
const translateX = useRef(new Animated.Value(0)).current;
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => !disabled,
onMoveShouldSetPanResponder: () => !disabled,
onPanResponderGrant: () => {
confirmed.current = false;
(translateX as any).setOffset((translateX as any)._value);
translateX.setValue(0);
},
onPanResponderMove: (_, gs) => {
const maxX = Math.max(trackWidth.current - HANDLE_SIZE - 6, 1);
const clamped = Math.min(Math.max(gs.dx, 0), maxX);
translateX.setValue(clamped - (translateX as any)._offset);
if (!confirmed.current && clamped / maxX >= THRESHOLD) {
confirmed.current = true;
(translateX as any).flattenOffset();
Animated.spring(translateX, {
toValue: maxX,
useNativeDriver: false,
damping: 18,
stiffness: 220,
}).start();
onConfirm();
}
},
onPanResponderRelease: () => {
(translateX as any).flattenOffset();
if (!confirmed.current) {
Animated.spring(translateX, {
toValue: 0,
useNativeDriver: false,
damping: 18,
stiffness: 220,
}).start();
}
},
onPanResponderTerminate: () => {
(translateX as any).flattenOffset();
if (!confirmed.current) {
Animated.spring(translateX, {
toValue: 0,
useNativeDriver: false,
}).start();
}
},
})
).current;
const maxXForOpacity = Math.max(trackWidth.current - HANDLE_SIZE - 6, 1);
const labelOpacity = translateX.interpolate({
inputRange: [0, maxXForOpacity > 0 ? maxXForOpacity * 0.6 : 1],
outputRange: [1, 0],
extrapolate: 'clamp',
});
return (
<View
style={[styles.track, disabled && styles.trackDisabled, color && !disabled ? { backgroundColor: color } : undefined, style]}
onLayout={e => { trackWidth.current = e.nativeEvent.layout.width; }}
>
<Animated.View style={[StyleSheet.absoluteFill, styles.labelWrap, { opacity: labelOpacity }]} pointerEvents="none">
<Text style={styles.label}>{label}</Text>
</Animated.View>
<Animated.View
style={[styles.handle, disabled && styles.handleDisabled, { transform: [{ translateX }] }]}
{...panResponder.panHandlers}
collapsable={false}
>
<Text style={styles.arrow}></Text>
</Animated.View>
</View>
);
}
const styles = StyleSheet.create({
track: {
height: 60,
backgroundColor: '#0E7490',
borderRadius: 30,
justifyContent: 'center',
overflow: 'hidden',
},
trackDisabled: { backgroundColor: '#94A3B8' },
labelWrap: { alignItems: 'center', justifyContent: 'center' },
label: {
color: 'rgba(255,255,255,0.85)',
fontSize: 15,
fontWeight: '600',
letterSpacing: 0.3,
},
handle: {
width: HANDLE_SIZE,
height: HANDLE_SIZE,
borderRadius: HANDLE_SIZE / 2,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
marginLeft: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.18,
shadowRadius: 4,
elevation: 4,
},
handleDisabled: { backgroundColor: '#E2E8F0' },
arrow: {
fontSize: 28,
color: '#0891B2',
fontWeight: '800',
lineHeight: 32,
},
});