Files
aliasvault/apps/mobile-app/components/common/ModalWrapper.tsx
2026-01-22 22:28:36 +01:00

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;