import { useBackHandler } from '@react-native-community/hooks'; import * as Haptics from 'expo-haptics'; import React, { forwardRef, isValidElement, useEffect, useImperativeHandle, useRef, useState, useCallback } from 'react'; import { Keyboard, useWindowDimensions } from 'react-native'; import { Easing, useDerivedValue, useSharedValue } from 'react-native-reanimated'; import BottomSheet, { BottomSheetBackdrop } from '@gorhom/bottom-sheet'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../../theme'; import { isIOS, isTablet } from '../../lib/methods/helpers'; import { Handle } from './Handle'; import { TActionSheetOptions } from './Provider'; import BottomSheetContent from './BottomSheetContent'; import styles, { ITEM_HEIGHT } from './styles'; export const ACTION_SHEET_ANIMATION_DURATION = 250; const HANDLE_HEIGHT = 28; const CANCEL_HEIGHT = 64; const ANIMATION_CONFIG = { duration: ACTION_SHEET_ANIMATION_DURATION, // https://easings.net/#easeInOutCubic easing: Easing.bezier(0.645, 0.045, 0.355, 1.0) }; const ActionSheet = React.memo( forwardRef(({ children }: { children: React.ReactElement }, ref) => { const { colors } = useTheme(); const { height: windowHeight } = useWindowDimensions(); const { bottom } = useSafeAreaInsets(); const bottomSheetRef = useRef(null); const [data, setData] = useState({} as TActionSheetOptions); const [isVisible, setVisible] = useState(false); const animatedContentHeight = useSharedValue(0); const animatedHandleHeight = useSharedValue(0); const animatedDataSnaps = useSharedValue([]); const animatedSnapPoints = useDerivedValue(() => { if (animatedDataSnaps.value?.length) { return animatedDataSnaps.value; } const contentWithHandleHeight = animatedContentHeight.value + animatedHandleHeight.value; // Bottom sheet requires a default value to work if (contentWithHandleHeight === 0) { return ['25%']; } return [contentWithHandleHeight]; }, [data]); const handleContentLayout = useCallback( ({ nativeEvent: { layout: { height } } }) => { /** * This logic is only necessary to prevent the action sheet from * occupying the entire screen when the dynamic content is too big. */ animatedContentHeight.value = Math.min(height, windowHeight * 0.8); }, [animatedContentHeight, windowHeight] ); const maxSnap = Math.min( (ITEM_HEIGHT + 0.5) * (data?.options?.length || 0) + HANDLE_HEIGHT + // Custom header height (data?.headerHeight || 0) + // Insets bottom height (Notch devices) bottom + // Cancel button height (data?.hasCancel ? CANCEL_HEIGHT : 0), windowHeight * 0.8 ); /* * if the action sheet cover more than 60% of the screen height, * we'll provide more one snap of 50% */ const snaps = maxSnap > windowHeight * 0.6 && !data.snaps ? ['50%', maxSnap] : [maxSnap]; const toggleVisible = () => setVisible(!isVisible); const hide = () => { bottomSheetRef.current?.close(); }; const show = (options: TActionSheetOptions) => { setData(options); if (options.snaps?.length) { animatedDataSnaps.value = options.snaps; } toggleVisible(); }; useBackHandler(() => { if (isVisible) { hide(); } return isVisible; }); useEffect(() => { if (isVisible) { Keyboard.dismiss(); Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); } }, [isVisible]); useImperativeHandle(ref, () => ({ showActionSheet: show, hideActionSheet: hide })); const renderHandle = () => ( <> {isValidElement(data?.customHeader) ? data.customHeader : null} ); const onClose = () => { toggleVisible(); data?.onClose && data?.onClose(); animatedDataSnaps.value = []; }; const renderBackdrop = useCallback( props => ( ), [] ); const bottomSheet = isTablet ? styles.bottomSheet : {}; // Must need this prop to avoid keyboard dismiss // when is android tablet and the input text is focused const androidTablet: any = isTablet && !isIOS ? { android_keyboardInputMode: 'adjustResize' } : {}; return ( <> {children} {isVisible && ( index === -1 && onClose()} // We need this to allow horizontal swipe gesture inside the bottom sheet like in reaction picker enableContentPanningGesture={data?.enableContentPanningGesture ?? true} {...androidTablet} > )} ); }) ); export default ActionSheet;