mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-02-20 16:04:23 -05:00
193 lines
4.8 KiB
TypeScript
193 lines
4.8 KiB
TypeScript
import React from 'react';
|
|
import {
|
|
Modal,
|
|
StyleSheet,
|
|
Text,
|
|
View,
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
TouchableWithoutFeedback,
|
|
Keyboard,
|
|
ScrollView,
|
|
} from 'react-native';
|
|
|
|
import { useColors } from '@/hooks/useColorScheme';
|
|
|
|
interface IModalWrapperProps {
|
|
/** Whether the modal is visible. */
|
|
isOpen: boolean;
|
|
/** Callback when the modal is requested to close. */
|
|
onClose: () => void;
|
|
/** Whether closing is disabled (e.g., during submission). */
|
|
isSubmitting?: boolean;
|
|
/** Optional modal title. */
|
|
title?: string;
|
|
/** Modal body content. */
|
|
children: React.ReactNode;
|
|
/** Optional footer content (e.g., buttons). */
|
|
footer?: React.ReactNode;
|
|
/** Whether to enable keyboard avoiding behavior. Default: false. */
|
|
keyboardAvoiding?: boolean;
|
|
/** Whether the content should be scrollable. Default: false. */
|
|
scrollable?: boolean;
|
|
/** Maximum height for scrollable content. Default: 400. */
|
|
maxScrollHeight?: number;
|
|
/** Animation type for the modal. Default: 'fade'. */
|
|
animationType?: 'fade' | 'slide' | 'none';
|
|
/** Custom max width for the container. Default: 400. */
|
|
maxWidth?: number;
|
|
/** Custom width percentage. Default: '90%'. */
|
|
width?: string;
|
|
/** Whether to show header border. Default: true (only when title is provided). */
|
|
showHeaderBorder?: boolean;
|
|
/** Whether to show footer border. Default: true (only when footer is provided). */
|
|
showFooterBorder?: boolean;
|
|
}
|
|
|
|
/**
|
|
* A generic modal wrapper component that provides consistent behavior:
|
|
* - Themed backdrop (dark/light mode support)
|
|
* - Consistent container styling
|
|
* - Optional title with header
|
|
* - Optional scrollable content
|
|
* - Optional keyboard avoiding behavior
|
|
* - Prevents closing during submission
|
|
*/
|
|
export const ModalWrapper: React.FC<IModalWrapperProps> = ({
|
|
isOpen,
|
|
onClose,
|
|
isSubmitting = false,
|
|
title,
|
|
children,
|
|
footer,
|
|
keyboardAvoiding = false,
|
|
scrollable = false,
|
|
maxScrollHeight = 400,
|
|
animationType = 'fade',
|
|
maxWidth = 400,
|
|
width = '90%',
|
|
showHeaderBorder = true,
|
|
showFooterBorder = true,
|
|
}) => {
|
|
const colors = useColors();
|
|
|
|
/**
|
|
* Handle close - only allow if not submitting.
|
|
*/
|
|
const handleClose = (): void => {
|
|
if (!isSubmitting) {
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
backdrop: {
|
|
alignItems: 'center',
|
|
// Lighter backdrop in dark mode for better contrast against black background
|
|
backgroundColor: 'rgba(0, 0, 0, 0.65)',
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
},
|
|
container: {
|
|
backgroundColor: colors.background,
|
|
borderColor: colors.accentBorder,
|
|
borderRadius: 12,
|
|
borderWidth: 1,
|
|
elevation: 10,
|
|
marginHorizontal: 16,
|
|
maxWidth,
|
|
overflow: 'hidden',
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 4 },
|
|
shadowOpacity: 0.3,
|
|
shadowRadius: 8,
|
|
width: width as never,
|
|
},
|
|
header: {
|
|
borderBottomColor: showHeaderBorder ? colors.accentBorder : 'transparent',
|
|
borderBottomWidth: showHeaderBorder ? 1 : 0,
|
|
padding: 20,
|
|
paddingBottom: 0,
|
|
},
|
|
title: {
|
|
color: colors.text,
|
|
fontSize: 18,
|
|
fontWeight: '600',
|
|
},
|
|
body: {
|
|
padding: 20,
|
|
},
|
|
scrollBody: {
|
|
maxHeight: maxScrollHeight,
|
|
padding: 20,
|
|
},
|
|
footer: {
|
|
borderTopColor: showFooterBorder ? colors.accentBorder : 'transparent',
|
|
borderTopWidth: showFooterBorder ? 1 : 0,
|
|
padding: 16,
|
|
},
|
|
});
|
|
|
|
const renderContent = (): React.ReactNode => (
|
|
<View style={styles.container}>
|
|
{title && (
|
|
<View style={styles.header}>
|
|
<Text style={styles.title}>{title}</Text>
|
|
</View>
|
|
)}
|
|
|
|
{scrollable ? (
|
|
<ScrollView style={styles.scrollBody}>
|
|
{children}
|
|
</ScrollView>
|
|
) : (
|
|
<View style={styles.body}>
|
|
{children}
|
|
</View>
|
|
)}
|
|
|
|
{footer && (
|
|
<View style={styles.footer}>
|
|
{footer}
|
|
</View>
|
|
)}
|
|
</View>
|
|
);
|
|
|
|
const renderBackdrop = (): React.ReactNode => {
|
|
if (keyboardAvoiding) {
|
|
return (
|
|
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
|
<KeyboardAvoidingView
|
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
style={styles.backdrop}
|
|
>
|
|
<TouchableWithoutFeedback>
|
|
{renderContent()}
|
|
</TouchableWithoutFeedback>
|
|
</KeyboardAvoidingView>
|
|
</TouchableWithoutFeedback>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={styles.backdrop}>
|
|
{renderContent()}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
visible={isOpen}
|
|
transparent
|
|
animationType={animationType}
|
|
onRequestClose={handleClose}
|
|
>
|
|
{renderBackdrop()}
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default ModalWrapper;
|