2024-08-24 17:53:33 +02:00
|
|
|
import React, { forwardRef, useImperativeHandle, useRef, ReactElement, ComponentType, ReactNode } from 'react';
|
2024-07-02 16:41:17 +02:00
|
|
|
import { SheetSize, SizeInfo, TrueSheet, TrueSheetProps } from '@lodev09/react-native-true-sheet';
|
2024-08-24 01:53:44 +02:00
|
|
|
import { Keyboard, StyleSheet, View, TouchableOpacity, Platform, GestureResponderEvent, Text } from 'react-native';
|
2024-08-23 19:40:27 +02:00
|
|
|
import Ionicons from 'react-native-vector-icons/Ionicons';
|
|
|
|
import SaveFileButton from './SaveFileButton';
|
2024-08-24 01:53:44 +02:00
|
|
|
import { useTheme } from './themes';
|
|
|
|
import { Image } from '@rneui/base';
|
2024-05-20 11:54:13 +02:00
|
|
|
|
2024-06-30 19:17:55 +02:00
|
|
|
interface BottomModalProps extends TrueSheetProps {
|
2024-02-10 05:35:24 +01:00
|
|
|
children?: React.ReactNode;
|
2024-06-30 19:17:55 +02:00
|
|
|
onClose?: () => void;
|
2024-08-24 01:53:44 +02:00
|
|
|
onCloseModalPressed?: () => Promise<void>;
|
2024-06-30 19:17:55 +02:00
|
|
|
isGrabberVisible?: boolean;
|
2024-07-01 00:27:37 +02:00
|
|
|
sizes?: SheetSize[] | undefined;
|
2024-08-02 19:08:14 +02:00
|
|
|
footer?: ReactElement | ComponentType<any> | null;
|
2024-07-01 18:58:19 +02:00
|
|
|
footerDefaultMargins?: boolean | number;
|
2024-07-02 16:41:17 +02:00
|
|
|
onPresent?: () => void;
|
|
|
|
onSizeChange?: (size: SizeInfo) => void;
|
2024-07-06 22:58:42 +02:00
|
|
|
showCloseButton?: boolean;
|
2024-08-23 19:40:27 +02:00
|
|
|
shareContent?: BottomModalShareContent;
|
|
|
|
shareButtonOnPress?: (event: GestureResponderEvent) => void;
|
|
|
|
header?: ReactElement | ComponentType<any> | null;
|
2024-08-24 01:53:44 +02:00
|
|
|
headerTitle?: string;
|
|
|
|
headerSubtitle?: string;
|
2024-02-10 05:35:24 +01:00
|
|
|
}
|
|
|
|
|
2024-08-23 19:40:27 +02:00
|
|
|
type BottomModalShareContent = {
|
|
|
|
fileName: string;
|
|
|
|
fileContent: string;
|
|
|
|
};
|
|
|
|
|
2024-06-30 19:17:55 +02:00
|
|
|
export interface BottomModalHandle {
|
|
|
|
present: () => Promise<void>;
|
|
|
|
dismiss: () => Promise<void>;
|
|
|
|
}
|
|
|
|
|
|
|
|
const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
|
2024-07-01 18:58:19 +02:00
|
|
|
(
|
2024-07-06 22:58:42 +02:00
|
|
|
{
|
|
|
|
onClose,
|
2024-08-24 01:53:44 +02:00
|
|
|
onCloseModalPressed,
|
2024-07-06 22:58:42 +02:00
|
|
|
onPresent,
|
|
|
|
onSizeChange,
|
|
|
|
showCloseButton = true,
|
|
|
|
isGrabberVisible = true,
|
2024-08-23 19:40:27 +02:00
|
|
|
shareContent,
|
|
|
|
shareButtonOnPress,
|
2024-07-06 22:58:42 +02:00
|
|
|
sizes = ['auto'],
|
|
|
|
footer,
|
|
|
|
footerDefaultMargins,
|
2024-08-23 19:40:27 +02:00
|
|
|
header,
|
2024-08-24 01:53:44 +02:00
|
|
|
headerTitle,
|
|
|
|
headerSubtitle,
|
2024-07-06 22:58:42 +02:00
|
|
|
children,
|
|
|
|
...props
|
|
|
|
},
|
2024-07-01 18:58:19 +02:00
|
|
|
ref,
|
|
|
|
) => {
|
2024-06-30 19:17:55 +02:00
|
|
|
const trueSheetRef = useRef<TrueSheet>(null);
|
2024-08-24 01:53:44 +02:00
|
|
|
const { colors } = useTheme();
|
|
|
|
const stylesHook = StyleSheet.create({
|
|
|
|
barButton: {
|
|
|
|
backgroundColor: colors.lightButton,
|
|
|
|
},
|
|
|
|
headerTitle: {
|
|
|
|
color: colors.foregroundColor,
|
|
|
|
},
|
|
|
|
});
|
2024-02-10 05:35:24 +01:00
|
|
|
|
2024-06-30 19:17:55 +02:00
|
|
|
useImperativeHandle(ref, () => ({
|
|
|
|
present: async () => {
|
2024-07-04 21:55:58 +02:00
|
|
|
Keyboard.dismiss();
|
2024-06-30 19:17:55 +02:00
|
|
|
if (trueSheetRef.current?.present) {
|
|
|
|
await trueSheetRef.current.present();
|
|
|
|
} else {
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
dismiss: async () => {
|
2024-07-04 21:55:58 +02:00
|
|
|
Keyboard.dismiss();
|
2024-06-30 19:17:55 +02:00
|
|
|
if (trueSheetRef.current?.dismiss) {
|
|
|
|
await trueSheetRef.current.dismiss();
|
|
|
|
} else {
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}));
|
|
|
|
|
2024-08-24 01:53:44 +02:00
|
|
|
const dismiss = async () => {
|
2024-08-24 20:08:00 +02:00
|
|
|
try {
|
|
|
|
await onCloseModalPressed?.();
|
|
|
|
await trueSheetRef.current?.dismiss();
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Error during dismiss:', error);
|
|
|
|
}
|
2024-07-06 22:58:42 +02:00
|
|
|
};
|
|
|
|
|
2024-08-23 19:40:27 +02:00
|
|
|
const renderTopRightButton = () => {
|
|
|
|
const buttons = [];
|
|
|
|
if (shareContent || shareButtonOnPress) {
|
|
|
|
if (shareContent) {
|
|
|
|
buttons.push(
|
|
|
|
<SaveFileButton
|
2024-08-24 01:53:44 +02:00
|
|
|
style={[styles.topRightButton, stylesHook.barButton]}
|
2024-08-23 19:40:27 +02:00
|
|
|
fileContent={shareContent.fileContent}
|
|
|
|
fileName={shareContent.fileName}
|
|
|
|
testID="ModalShareButton"
|
2024-08-24 01:53:44 +02:00
|
|
|
key="ModalShareButton"
|
2024-08-23 19:40:27 +02:00
|
|
|
>
|
2024-08-24 01:53:44 +02:00
|
|
|
<Ionicons name="share" size={20} color={colors.buttonTextColor} />
|
2024-08-23 19:40:27 +02:00
|
|
|
</SaveFileButton>,
|
|
|
|
);
|
|
|
|
} else if (shareButtonOnPress) {
|
|
|
|
buttons.push(
|
2024-08-24 01:53:44 +02:00
|
|
|
<TouchableOpacity
|
|
|
|
testID="ModalShareButton"
|
|
|
|
key="ModalShareButton"
|
|
|
|
style={[styles.topRightButton, stylesHook.barButton]}
|
|
|
|
onPress={shareButtonOnPress}
|
|
|
|
>
|
|
|
|
<Ionicons name="share" size={20} color={colors.buttonTextColor} />
|
2024-08-23 19:40:27 +02:00
|
|
|
</TouchableOpacity>,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (showCloseButton) {
|
|
|
|
buttons.push(
|
2024-08-24 01:53:44 +02:00
|
|
|
<TouchableOpacity
|
|
|
|
style={[styles.topRightButton, stylesHook.barButton]}
|
|
|
|
onPress={dismiss}
|
|
|
|
key="ModalDoneButton"
|
|
|
|
testID="ModalDoneButton"
|
|
|
|
>
|
|
|
|
{Platform.OS === 'ios' ? (
|
|
|
|
<Ionicons name="close" size={20} color={colors.buttonTextColor} />
|
|
|
|
) : (
|
|
|
|
<Image source={require('../img/close.png')} style={styles.closeButton} />
|
|
|
|
)}
|
2024-08-23 19:40:27 +02:00
|
|
|
</TouchableOpacity>,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return <View style={styles.topRightButtonContainer}>{buttons}</View>;
|
|
|
|
};
|
|
|
|
|
|
|
|
const renderHeader = () => {
|
2024-08-24 01:53:44 +02:00
|
|
|
if (headerTitle || headerSubtitle) {
|
|
|
|
return (
|
|
|
|
<View style={styles.headerContainer}>
|
|
|
|
<View style={styles.headerContent}>
|
|
|
|
{headerTitle && <Text style={[styles.headerTitle, stylesHook.headerTitle]}>{headerTitle}</Text>}
|
|
|
|
{headerSubtitle && <Text style={[styles.headerSubtitle, stylesHook.headerTitle]}>{headerSubtitle}</Text>}
|
|
|
|
</View>
|
|
|
|
{renderTopRightButton()}
|
|
|
|
</View>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-08-23 19:40:27 +02:00
|
|
|
return (
|
|
|
|
<View style={styles.headerContainer}>
|
|
|
|
<View style={styles.headerContent}>{typeof header === 'function' ? <header /> : header}</View>
|
|
|
|
{renderTopRightButton()}
|
|
|
|
</View>
|
|
|
|
);
|
|
|
|
};
|
2024-07-06 22:58:42 +02:00
|
|
|
|
2024-08-23 19:40:27 +02:00
|
|
|
const renderFooter = (): ReactElement | undefined => {
|
2024-07-06 22:58:42 +02:00
|
|
|
// Footer is not working correctly on Android yet.
|
|
|
|
if (!footer) return undefined;
|
2024-07-01 18:58:19 +02:00
|
|
|
|
|
|
|
if (React.isValidElement(footer)) {
|
2024-07-06 22:58:42 +02:00
|
|
|
return footerDefaultMargins ? <View style={styles.footerContainer}>{footer}</View> : footer;
|
|
|
|
} else if (typeof footer === 'function') {
|
2024-08-24 19:35:13 +02:00
|
|
|
const ModalFooterComponent = footer as ComponentType<any>;
|
|
|
|
return <ModalFooterComponent />;
|
2024-07-01 18:58:19 +02:00
|
|
|
}
|
2024-07-06 22:58:42 +02:00
|
|
|
|
|
|
|
return undefined;
|
|
|
|
};
|
2024-07-01 18:58:19 +02:00
|
|
|
|
2024-08-24 17:53:33 +02:00
|
|
|
const FooterComponent = Platform.OS !== 'android' && renderFooter();
|
2024-07-06 23:55:03 +02:00
|
|
|
|
2024-06-30 19:17:55 +02:00
|
|
|
return (
|
|
|
|
<TrueSheet
|
|
|
|
ref={trueSheetRef}
|
2024-07-01 00:27:37 +02:00
|
|
|
sizes={sizes}
|
2024-06-30 19:17:55 +02:00
|
|
|
onDismiss={onClose}
|
|
|
|
onPresent={onPresent}
|
|
|
|
onSizeChange={onSizeChange}
|
|
|
|
grabber={isGrabberVisible}
|
2024-08-24 17:53:33 +02:00
|
|
|
FooterComponent={FooterComponent as ReactElement}
|
2024-06-30 19:17:55 +02:00
|
|
|
{...props}
|
|
|
|
>
|
2024-08-23 19:40:27 +02:00
|
|
|
<View style={styles.childrenContainer}>{children}</View>
|
2024-08-24 17:53:33 +02:00
|
|
|
{Platform.OS === 'android' && (renderFooter() as ReactNode)}
|
2024-08-24 19:35:13 +02:00
|
|
|
{renderHeader()}
|
2024-06-30 19:17:55 +02:00
|
|
|
</TrueSheet>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
2020-11-17 09:43:38 +01:00
|
|
|
|
|
|
|
export default BottomModal;
|
2024-08-23 19:40:27 +02:00
|
|
|
|
|
|
|
const styles = StyleSheet.create({
|
|
|
|
footerContainer: {
|
|
|
|
alignItems: 'center',
|
|
|
|
justifyContent: 'center',
|
|
|
|
},
|
|
|
|
headerContainer: {
|
2024-08-24 19:35:13 +02:00
|
|
|
position: 'absolute',
|
2024-08-23 19:40:27 +02:00
|
|
|
flexDirection: 'row',
|
|
|
|
alignItems: 'center',
|
2024-08-24 01:53:44 +02:00
|
|
|
paddingVertical: 8,
|
2024-08-23 19:40:27 +02:00
|
|
|
minHeight: 22,
|
2024-08-24 19:35:13 +02:00
|
|
|
right: 16,
|
|
|
|
top: 16,
|
2024-08-23 19:40:27 +02:00
|
|
|
},
|
|
|
|
headerContent: {
|
|
|
|
flex: 1,
|
|
|
|
justifyContent: 'center',
|
|
|
|
minHeight: 22,
|
|
|
|
},
|
2024-08-24 01:53:44 +02:00
|
|
|
headerTitle: {
|
|
|
|
fontSize: 18,
|
|
|
|
fontWeight: 'bold',
|
|
|
|
},
|
|
|
|
closeButton: {
|
|
|
|
width: 10,
|
|
|
|
height: 10,
|
|
|
|
},
|
|
|
|
headerSubtitle: {
|
|
|
|
fontSize: 14,
|
|
|
|
},
|
2024-08-23 19:40:27 +02:00
|
|
|
topRightButton: {
|
|
|
|
justifyContent: 'center',
|
|
|
|
alignItems: 'center',
|
|
|
|
width: 30,
|
|
|
|
height: 30,
|
|
|
|
borderRadius: 15,
|
|
|
|
marginLeft: 22,
|
|
|
|
},
|
|
|
|
topRightButtonContainer: {
|
|
|
|
flexDirection: 'row',
|
|
|
|
alignItems: 'center',
|
|
|
|
},
|
|
|
|
childrenContainer: {
|
2024-08-24 19:35:13 +02:00
|
|
|
paddingTop: 66,
|
|
|
|
paddingHorizontal: 16,
|
2024-08-24 01:53:44 +02:00
|
|
|
width: '100%',
|
2024-08-23 19:40:27 +02:00
|
|
|
},
|
|
|
|
});
|