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 } from 'react-native'; import { Easing } from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import BottomSheet, { BottomSheetBackdrop } from '@gorhom/bottom-sheet'; import { useDimensions, useOrientation } from '../../dimensions'; 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'; const HANDLE_HEIGHT = isIOS ? 40 : 56; const MIN_SNAP_HEIGHT = 16; const CANCEL_HEIGHT = 64; export const ACTION_SHEET_ANIMATION_DURATION = 250; 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 bottomSheetRef = useRef(null); const [data, setData] = useState({} as TActionSheetOptions); const [isVisible, setVisible] = useState(false); const { height } = useDimensions(); const { isLandscape } = useOrientation(); const insets = useSafeAreaInsets(); const maxSnap = Math.min( // Items height ITEM_HEIGHT * (data?.options?.length || 0) + // Handle height HANDLE_HEIGHT + // Custom header height (data?.headerHeight || 0) + // Insets bottom height (Notch devices) insets.bottom + // Cancel button height (data?.hasCancel ? CANCEL_HEIGHT : 0), height - MIN_SNAP_HEIGHT ); /* * if the action sheet cover more * than 60% of the whole screen * and it's not at the landscape mode * we'll provide more one snap * that point 50% of the whole screen */ const snaps = maxSnap > height * 0.6 && !isLandscape && !data.snaps ? [height * 0.5, maxSnap] : [maxSnap]; const toggleVisible = () => setVisible(!isVisible); const hide = () => { bottomSheetRef.current?.close(); }; const show = (options: TActionSheetOptions) => { setData(options); toggleVisible(); }; useBackHandler(() => { if (isVisible) { hide(); } return isVisible; }); useEffect(() => { if (isVisible) { Keyboard.dismiss(); Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); } }, [isVisible]); // Hides action sheet when orientation changes useEffect(() => { setVisible(false); }, [isLandscape]); useImperativeHandle(ref, () => ({ showActionSheet: show, hideActionSheet: hide })); const renderHandle = () => ( <> {isValidElement(data?.customHeader) ? data.customHeader : null} ); const onClose = () => { toggleVisible(); data?.onClose && data?.onClose(); }; const renderBackdrop = useCallback( props => ( ), [] ); const bottomSheet = isLandscape || 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 && isLandscape && !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;