import React, { useCallback, useContext, useEffect, useRef } from 'react'; import { BackHandler, Dimensions, KeyboardAvoidingView, Modal, PanResponder, Platform, Pressable, StyleSheet, TouchableOpacity, View, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { ApplicationContext, MiniAppContext } from '../Context'; import { BottomSheetParams } from './types'; import { Colors, Radius, Spacing, Styles } from '../Consts'; import { Text } from '../Text'; import { Icon } from '../Icon'; import { useHeaderHeight } from '@react-navigation/elements'; import Animated, { Easing, Extrapolate, interpolate, runOnJS, useAnimatedStyle, useSharedValue, withSpring, withTiming, } from 'react-native-reanimated'; import layoutStyles from '../Layout/styles'; const BottomSheet: React.FC = props => { const { theme, navigator } = useContext(ApplicationContext); const context = useContext(MiniAppContext); const heightDevice = Dimensions.get('screen').height; const action = useRef(undefined); const insets = useSafeAreaInsets(); const heightHeader = useHeaderHeight(); const keyboardOffset = heightHeader - Math.min(insets.bottom, 21); const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false; const { screen: Screen, options, useNativeModal = false, surface, barrierDismissible = false, draggable = true, useBottomInset = true, useKeyboardAvoidingView = true, keyboardVerticalOffset, useDivider = true, footerComponent, leftOptions, }: BottomSheetParams = props.route.params; const translateY = useSharedValue(heightDevice); const openAnimation = useCallback(() => { translateY.value = withTiming(0, { duration: 350, easing: Easing.bezier(0.05, 0.7, 0.1, 1), }); }, [translateY]); const closeAnimation = useCallback( (callback = () => {}) => { translateY.value = withTiming( heightDevice, { duration: 200, easing: Easing.bezier(0.3, 0.0, 0.8, 0.15), }, () => runOnJS(callback)(), ); }, [heightDevice, translateY], ); const panResponder = useRef( PanResponder.create({ onStartShouldSetPanResponder: () => draggable, onMoveShouldSetPanResponder: (_, gestureState) => draggable && gestureState.dy > 0, onPanResponderMove: (_, gestureState) => { if (gestureState.dy > 0) { translateY.value = gestureState.dy; } }, onPanResponderRelease: (_, gestureState) => { if (gestureState.dy > 100) { action.current = 'gesture'; onDismiss(true); } else { translateY.value = withSpring(0, { damping: 20, mass: 1, stiffness: 100, overshootClamping: false, energyThreshold: 1e-10, }); } }, }), ).current; let Container: any = View; if (useNativeModal) { Container = Modal; } let backgroundColor = theme.colors.background.default; if (surface) { backgroundColor = theme.colors.background.surface; } /** * emit dismiss event */ useEffect(() => { translateY.value = heightDevice; openAnimation(); return () => { props.route.params?.onDismiss?.(action.current); }; }, [heightDevice, openAnimation, props.route.params, translateY]); /** * handler dismiss */ const onDismiss = useCallback( (force = false, callback?: () => void) => { if (barrierDismissible && !force) { return; } closeAnimation(() => { navigator?.pop(); runOnJS(() => { callback?.(); })(); }); }, [barrierDismissible, closeAnimation, navigator], ); /** * on request close */ const onRequestClose = useCallback( (callback?: () => void) => { onDismiss(true, callback); }, [onDismiss], ); useEffect(() => { const backHandler = BackHandler.addEventListener( 'hardwareBackPress', () => { onDismiss(); return true; }, ); return () => backHandler.remove(); }, [barrierDismissible, onDismiss]); /** * render header */ const renderHeader = useCallback(() => { return ( {leftOptions?.onPressIconLeft ? ( { onDismiss(true, leftOptions.onPressIconLeft); }} > ) : null} {options.header ? ( {options.header} ) : ( <> {!leftOptions ? : null} {options.title ?? ''} )} { action.current = 'icon_close'; onDismiss(true); }} > ); }, [ onDismiss, leftOptions, options.header, options.title, panResponder.panHandlers, theme.colors.border.default, useDivider, ]); const animatedStyle = useAnimatedStyle(() => { return { transform: [{ translateY: translateY.value }], }; }); const animatedOverlayStyle = useAnimatedStyle(() => { return { opacity: interpolate( translateY.value, [0, heightDevice], [1, 0], Extrapolate.CLAMP, ), }; }); return ( { onDismiss(); }} style={StyleSheet.absoluteFillObject} isModalKit={true} > { action.current = 'touch'; onDismiss(); }} > {renderHeader()} {footerComponent && ( {footerComponent} )} {useBottomInset && ( )} ); }; const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'flex-end', }, overlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.6)', }, indicator: { width: 40, height: 4, borderRadius: Radius.S, backgroundColor: Colors.black_06, position: 'absolute', alignSelf: 'center', top: Spacing.S, }, header: { height: 56, flexDirection: 'row', alignItems: 'center', }, iconButton: { width: 24, height: 24, alignItems: 'center', justifyContent: 'center', marginHorizontal: Spacing.M, }, debugBaseLine: { borderWidth: 1, borderColor: Colors.green_06 }, }); export default BottomSheet;