164 lines
4.8 KiB
TypeScript
164 lines
4.8 KiB
TypeScript
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;
|