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 { useTheme } from '../../theme'; import { isIOS, isTablet } from '../../lib/methods/helpers'; import { Handle } from './Handle'; import { TActionSheetOptions } from './Provider'; import BottomSheetContent from './BottomSheetContent'; import styles from './styles'; 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<BottomSheet>(null); const [data, setData] = useState<TActionSheetOptions>({} as TActionSheetOptions); const [isVisible, setVisible] = useState(false); const { width, height } = useWindowDimensions(); const isLandscape = width > height; const animatedContentHeight = useSharedValue(0); const animatedHandleHeight = useSharedValue(0); const animatedDataSnaps = useSharedValue<TActionSheetOptions['snaps']>([]); 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 } } }) => { animatedContentHeight.value = height; }, [animatedContentHeight] ); 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]); // Hides action sheet when orientation changes useEffect(() => { setVisible(false); }, [isLandscape]); useImperativeHandle(ref, () => ({ showActionSheet: show, hideActionSheet: hide })); const renderHandle = () => ( <> <Handle /> {isValidElement(data?.customHeader) ? data.customHeader : null} </> ); const onClose = () => { toggleVisible(); data?.onClose && data?.onClose(); animatedDataSnaps.value = []; }; const renderBackdrop = useCallback( props => ( <BottomSheetBackdrop {...props} appearsOnIndex={0} // Backdrop should be visible all the time bottom sheet is open disappearsOnIndex={-1} opacity={colors.backdropOpacity} /> ), [] ); 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 && ( <BottomSheet ref={bottomSheetRef} snapPoints={animatedSnapPoints} handleHeight={animatedHandleHeight} // We need undefined to enable vertical swipe gesture inside the bottom sheet like in reaction picker contentHeight={data.snaps?.length ? undefined : animatedContentHeight} animationConfigs={ANIMATION_CONFIG} animateOnMount={true} backdropComponent={renderBackdrop} handleComponent={renderHandle} enablePanDownToClose style={{ ...styles.container, ...bottomSheet }} backgroundStyle={{ backgroundColor: colors.focusedBackground }} onChange={index => index === -1 && onClose()} // We need this to allow horizontal swipe gesture inside the bottom sheet like in reaction picker enableContentPanningGesture={data?.enableContentPanningGesture ?? true} {...androidTablet} > <BottomSheetContent options={data?.options} hide={hide} children={data?.children} hasCancel={data?.hasCancel} onLayout={handleContentLayout} /> </BottomSheet> )} </> ); }) ); export default ActionSheet;