UNPKG

bottom-sheet-stepper

Version:

A lightweight and customizable stepper component for React Native, built on top of @gorhom/bottom-sheet. Easily manage multi-step flows in a modal bottom sheet with smooth animations and full control.

128 lines (127 loc) 5.25 kB
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState, } from 'react'; import { StyleSheet, Platform, } from 'react-native'; import { BottomSheetModal, BottomSheetView, ANIMATION_DURATION, ANIMATION_EASING, ANIMATION_CONFIGS, BottomSheetBackdrop, } from '@gorhom/bottom-sheet'; import Animated, { FadeIn, FadeOut, useAnimatedStyle, useSharedValue, withSpring, withTiming, } from 'react-native-reanimated'; const BottomSheetStepper = forwardRef(({ steps, style, bottomInset = 20, horizontalInset = 24, disablePanDownToClose }, ref) => { const [step, setStep] = useState(0); const height = useSharedValue(0); const transform = useSharedValue(0); const bottomSheetRef = useRef(null); useImperativeHandle(ref, () => ({ present: () => bottomSheetRef.current?.present(), dismiss: () => bottomSheetRef.current?.dismiss(), })); const handleClose = useCallback(() => { transform.value = height.value; bottomSheetRef.current?.dismiss(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [transform]); const afterClosed = useCallback(() => { setTimeout(() => { height.value = 0; transform.value = 0; setStep(0); }, 200); }, [height, transform]); const handlePressNext = useCallback(() => { if (step + 1 >= steps.length) { handleClose(); } setStep((prev) => prev + 1); }, [handleClose, step, steps.length]); const handlePressBack = useCallback(() => { if (step - 1 < 0) { handleClose(); } else { setStep((prev) => prev - 1); } }, [handleClose, step]); const animatedStyle = useAnimatedStyle(() => { return { height: Platform.OS === 'ios' ? withSpring(height.value, ANIMATION_CONFIGS) : withTiming(height.value, { duration: ANIMATION_DURATION, easing: ANIMATION_EASING, }), }; }); const containerAnimatedStyle = useAnimatedStyle(() => { return { transform: [ { translateY: Platform.OS === 'ios' ? withSpring(transform.value, ANIMATION_CONFIGS) : withTiming(transform.value, { duration: ANIMATION_DURATION, easing: ANIMATION_EASING, }), }, ], }; }); const renderBackdrop = useCallback((backdropProps) => (<BottomSheetBackdrop {...backdropProps} disappearsOnIndex={-1} appearsOnIndex={0} onPress={handleClose}/>), [handleClose]); const renderContent = useCallback(() => { const StepComponent = steps[step]; if (!StepComponent) return null; return (<Animated.View style={styles.animatedView} key={`step_${step}`} entering={FadeIn} exiting={FadeOut} onLayout={(e) => { const measuredHeight = e.nativeEvent.layout.height; height.value = measuredHeight; }}> {StepComponent({ onNextPress: handlePressNext, onBackPress: handlePressBack, onEnd: handleClose, })} </Animated.View>); }, [handleClose, handlePressBack, handlePressNext, height, step, steps]); return (<BottomSheetModal detached ref={bottomSheetRef} enableDynamicSizing={true} maxDynamicContentSize={100} android_keyboardInputMode="adjustResize" style={{ marginHorizontal: horizontalInset }} handleStyle={styles.bottomSheetHandle} containerStyle={styles.bottomSheetContainer} backgroundStyle={styles.bottomSheetBackground} backdropComponent={renderBackdrop} onChange={(index) => (index === -1 ? afterClosed() : undefined)} enablePanDownToClose={disablePanDownToClose ? false : undefined}> <BottomSheetView style={[styles.bottomSheetView, { paddingBottom: bottomInset }]}> <Animated.View style={[styles.contentContainer, style, containerAnimatedStyle]}> <Animated.View style={[styles.animatedBox, animatedStyle]}> {renderContent()} </Animated.View> </Animated.View> </BottomSheetView> </BottomSheetModal>); }); const styles = StyleSheet.create({ contentContainer: { alignItems: 'center', backgroundColor: 'white', padding: 16, borderRadius: 16, }, animatedBox: { width: '100%', overflow: 'hidden', alignItems: 'center', justifyContent: 'center', }, animatedView: { flex: 1, position: 'absolute', width: '100%', }, bottomSheetContainer: { backgroundColor: 'transparent', alignItems: 'flex-end', }, bottomSheetBackground: { borderRadius: 0, backgroundColor: 'transparent', alignItems: 'flex-end', }, bottomSheetHandle: { display: 'none', }, bottomSheetView: { backgroundColor: 'transparent', justifyContent: 'flex-end', marginTop: 'auto', }, }); BottomSheetStepper.displayName = 'BottomSheetStepper'; export default BottomSheetStepper;