remove gluestackk-ui
This commit is contained in:
578
components/ui/modal.tsx
Normal file
578
components/ui/modal.tsx
Normal file
@@ -0,0 +1,578 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import React, { ReactNode, useEffect, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
Pressable,
|
||||
Modal as RNModal,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
|
||||
// Types
|
||||
export interface ModalProps {
|
||||
/** Whether the modal dialog is visible or not */
|
||||
open?: boolean;
|
||||
/** The modal dialog's title */
|
||||
title?: ReactNode;
|
||||
/** Whether a close (x) button is visible on top right or not */
|
||||
closable?: boolean;
|
||||
/** Custom close icon */
|
||||
closeIcon?: ReactNode;
|
||||
/** Whether to close the modal dialog when the mask (area outside the modal) is clicked */
|
||||
maskClosable?: boolean;
|
||||
/** Centered Modal */
|
||||
centered?: boolean;
|
||||
/** Width of the modal dialog */
|
||||
width?: number | string;
|
||||
/** Whether to apply loading visual effect for OK button or not */
|
||||
confirmLoading?: boolean;
|
||||
/** Text of the OK button */
|
||||
okText?: string;
|
||||
/** Text of the Cancel button */
|
||||
cancelText?: string;
|
||||
/** Button type of the OK button */
|
||||
okType?: "primary" | "default" | "dashed" | "text" | "link";
|
||||
/** Footer content, set as footer={null} when you don't need default buttons */
|
||||
footer?: ReactNode | null;
|
||||
/** Whether show mask or not */
|
||||
mask?: boolean;
|
||||
/** The z-index of the Modal */
|
||||
zIndex?: number;
|
||||
/** Specify a function that will be called when a user clicks the OK button */
|
||||
onOk?: (e?: any) => void | Promise<void>;
|
||||
/** Specify a function that will be called when a user clicks mask, close button on top right or Cancel button */
|
||||
onCancel?: (e?: any) => void;
|
||||
/** Callback when the animation ends when Modal is turned on and off */
|
||||
afterOpenChange?: (open: boolean) => void;
|
||||
/** Specify a function that will be called when modal is closed completely */
|
||||
afterClose?: () => void;
|
||||
/** Custom className */
|
||||
className?: string;
|
||||
/** Modal body content */
|
||||
children?: ReactNode;
|
||||
/** Whether to unmount child components on close */
|
||||
destroyOnClose?: boolean;
|
||||
/** The ok button props */
|
||||
okButtonProps?: any;
|
||||
/** The cancel button props */
|
||||
cancelButtonProps?: any;
|
||||
/** Whether support press esc to close */
|
||||
keyboard?: boolean;
|
||||
}
|
||||
|
||||
export interface ConfirmModalProps extends Omit<ModalProps, "open"> {
|
||||
/** Type of the confirm modal */
|
||||
type?: "info" | "success" | "error" | "warning" | "confirm";
|
||||
/** Content */
|
||||
content?: ReactNode;
|
||||
/** Custom icon */
|
||||
icon?: ReactNode;
|
||||
}
|
||||
|
||||
// Modal Component
|
||||
const Modal: React.FC<ModalProps> & {
|
||||
info: (props: ConfirmModalProps) => ModalInstance;
|
||||
success: (props: ConfirmModalProps) => ModalInstance;
|
||||
error: (props: ConfirmModalProps) => ModalInstance;
|
||||
warning: (props: ConfirmModalProps) => ModalInstance;
|
||||
confirm: (props: ConfirmModalProps) => ModalInstance;
|
||||
useModal: () => [
|
||||
{
|
||||
info: (props: ConfirmModalProps) => ModalInstance;
|
||||
success: (props: ConfirmModalProps) => ModalInstance;
|
||||
error: (props: ConfirmModalProps) => ModalInstance;
|
||||
warning: (props: ConfirmModalProps) => ModalInstance;
|
||||
confirm: (props: ConfirmModalProps) => ModalInstance;
|
||||
},
|
||||
ReactNode
|
||||
];
|
||||
} = ({
|
||||
open = false,
|
||||
title,
|
||||
closable = true,
|
||||
closeIcon,
|
||||
maskClosable = true,
|
||||
centered = false,
|
||||
width = 520,
|
||||
confirmLoading = false,
|
||||
okText = "OK",
|
||||
cancelText = "Cancel",
|
||||
okType = "primary",
|
||||
footer,
|
||||
mask = true,
|
||||
zIndex = 1000,
|
||||
onOk,
|
||||
onCancel,
|
||||
afterOpenChange,
|
||||
afterClose,
|
||||
className,
|
||||
children,
|
||||
destroyOnClose = false,
|
||||
okButtonProps,
|
||||
cancelButtonProps,
|
||||
keyboard = true,
|
||||
}) => {
|
||||
const [visible, setVisible] = useState(open);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setVisible(open);
|
||||
if (afterOpenChange) {
|
||||
afterOpenChange(open);
|
||||
}
|
||||
}, [open, afterOpenChange]);
|
||||
|
||||
const handleOk = async () => {
|
||||
if (onOk) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await onOk();
|
||||
// Không tự động đóng modal - để parent component quyết định
|
||||
} catch (error) {
|
||||
console.error("Modal onOk error:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} else {
|
||||
setVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
if (onCancel) {
|
||||
onCancel();
|
||||
} else {
|
||||
// Nếu không có onCancel, tự động đóng modal
|
||||
setVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMaskPress = () => {
|
||||
if (maskClosable) {
|
||||
handleCancel();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRequestClose = () => {
|
||||
if (keyboard) {
|
||||
handleCancel();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible && afterClose) {
|
||||
const timer = setTimeout(() => {
|
||||
afterClose();
|
||||
}, 300); // Wait for animation to complete
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [visible, afterClose]);
|
||||
|
||||
const renderFooter = () => {
|
||||
if (footer === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (footer !== undefined) {
|
||||
return <View style={styles.footer}>{footer}</View>;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.footer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.cancelButton]}
|
||||
onPress={handleCancel}
|
||||
disabled={loading || confirmLoading}
|
||||
{...cancelButtonProps}
|
||||
>
|
||||
<Text style={styles.cancelButtonText}>{cancelText}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.button,
|
||||
styles.okButton,
|
||||
okType === "primary" && styles.primaryButton,
|
||||
(loading || confirmLoading) && styles.disabledButton,
|
||||
]}
|
||||
onPress={handleOk}
|
||||
disabled={loading || confirmLoading}
|
||||
{...okButtonProps}
|
||||
>
|
||||
{loading || confirmLoading ? (
|
||||
<ActivityIndicator color="#fff" size="small" />
|
||||
) : (
|
||||
<Text style={styles.okButtonText}>{okText}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const modalWidth =
|
||||
typeof width === "number" ? width : Dimensions.get("window").width * 0.9;
|
||||
|
||||
return (
|
||||
<RNModal
|
||||
visible={visible}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={handleRequestClose}
|
||||
statusBarTranslucent
|
||||
>
|
||||
<Pressable
|
||||
style={[
|
||||
styles.overlay,
|
||||
centered && styles.centered,
|
||||
{ zIndex },
|
||||
!mask && styles.noMask,
|
||||
]}
|
||||
onPress={handleMaskPress}
|
||||
>
|
||||
<Pressable
|
||||
style={[styles.modal, { width: modalWidth, maxWidth: "90%" }]}
|
||||
onPress={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
{(title || closable) && (
|
||||
<View style={styles.header}>
|
||||
{title && (
|
||||
<Text style={styles.title} numberOfLines={1}>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
{closable && (
|
||||
<TouchableOpacity
|
||||
style={styles.closeButton}
|
||||
onPress={handleCancel}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
{closeIcon || (
|
||||
<Ionicons name="close" size={24} color="#666" />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Body */}
|
||||
<View style={styles.body}>
|
||||
{(!destroyOnClose || visible) && children}
|
||||
</View>
|
||||
|
||||
{/* Footer */}
|
||||
{renderFooter()}
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
</RNModal>
|
||||
);
|
||||
};
|
||||
|
||||
// Confirm Modal Component
|
||||
const ConfirmModal: React.FC<
|
||||
ConfirmModalProps & { visible: boolean; onClose: () => void }
|
||||
> = ({
|
||||
visible,
|
||||
onClose,
|
||||
type = "confirm",
|
||||
title,
|
||||
content,
|
||||
icon,
|
||||
okText = "OK",
|
||||
cancelText = "Cancel",
|
||||
onOk,
|
||||
onCancel,
|
||||
...restProps
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const getIcon = () => {
|
||||
if (icon !== undefined) return icon;
|
||||
|
||||
const iconProps = { size: 24, style: { marginRight: 12 } };
|
||||
switch (type) {
|
||||
case "info":
|
||||
return (
|
||||
<Ionicons name="information-circle" color="#1890ff" {...iconProps} />
|
||||
);
|
||||
case "success":
|
||||
return (
|
||||
<Ionicons name="checkmark-circle" color="#52c41a" {...iconProps} />
|
||||
);
|
||||
case "error":
|
||||
return <Ionicons name="close-circle" color="#ff4d4f" {...iconProps} />;
|
||||
case "warning":
|
||||
return <Ionicons name="warning" color="#faad14" {...iconProps} />;
|
||||
default:
|
||||
return <Ionicons name="help-circle" color="#1890ff" {...iconProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
const handleOk = async () => {
|
||||
if (onOk) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await onOk();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Confirm modal onOk error:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
if (onCancel) {
|
||||
onCancel();
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={visible}
|
||||
title={title}
|
||||
onOk={handleOk}
|
||||
onCancel={type === "confirm" ? handleCancel : undefined}
|
||||
okText={okText}
|
||||
cancelText={cancelText}
|
||||
confirmLoading={loading}
|
||||
footer={
|
||||
type === "confirm" ? undefined : (
|
||||
<View style={styles.footer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.okButton, styles.primaryButton]}
|
||||
onPress={handleOk}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" size="small" />
|
||||
) : (
|
||||
<Text style={styles.okButtonText}>{okText}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
{...restProps}
|
||||
>
|
||||
<View style={styles.confirmContent}>
|
||||
{getIcon()}
|
||||
<Text style={styles.confirmText}>{content}</Text>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
// Modal Instance
|
||||
export interface ModalInstance {
|
||||
destroy: () => void;
|
||||
update: (config: ConfirmModalProps) => void;
|
||||
}
|
||||
|
||||
// Container for imperatively created modals - Not used in React Native
|
||||
// Static methods will return instance but won't render imperatively
|
||||
// Use Modal.useModal() hook for proper context support
|
||||
|
||||
const createConfirmModal = (config: ConfirmModalProps): ModalInstance => {
|
||||
console.warn(
|
||||
"Modal static methods are not fully supported in React Native. Please use Modal.useModal() hook for better context support."
|
||||
);
|
||||
|
||||
return {
|
||||
destroy: () => {
|
||||
console.warn(
|
||||
"Modal.destroy() called but static modals are not supported in React Native"
|
||||
);
|
||||
},
|
||||
update: (newConfig: ConfirmModalProps) => {
|
||||
console.warn(
|
||||
"Modal.update() called but static modals are not supported in React Native"
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Static methods
|
||||
Modal.info = (props: ConfirmModalProps) =>
|
||||
createConfirmModal({ ...props, type: "info" });
|
||||
Modal.success = (props: ConfirmModalProps) =>
|
||||
createConfirmModal({ ...props, type: "success" });
|
||||
Modal.error = (props: ConfirmModalProps) =>
|
||||
createConfirmModal({ ...props, type: "error" });
|
||||
Modal.warning = (props: ConfirmModalProps) =>
|
||||
createConfirmModal({ ...props, type: "warning" });
|
||||
Modal.confirm = (props: ConfirmModalProps) =>
|
||||
createConfirmModal({ ...props, type: "confirm" });
|
||||
|
||||
// useModal hook
|
||||
Modal.useModal = () => {
|
||||
const [modals, setModals] = useState<ReactNode[]>([]);
|
||||
|
||||
const createModal = (
|
||||
config: ConfirmModalProps,
|
||||
type: ConfirmModalProps["type"]
|
||||
) => {
|
||||
const id = `modal-${Date.now()}-${Math.random()}`;
|
||||
|
||||
const destroy = () => {
|
||||
setModals((prev) => prev.filter((modal: any) => modal.key !== id));
|
||||
};
|
||||
|
||||
const update = (newConfig: ConfirmModalProps) => {
|
||||
setModals((prev) =>
|
||||
prev.map((modal: any) =>
|
||||
modal.key === id ? (
|
||||
<ConfirmModal
|
||||
key={id}
|
||||
visible={true}
|
||||
onClose={destroy}
|
||||
{...config}
|
||||
{...newConfig}
|
||||
type={type}
|
||||
/>
|
||||
) : (
|
||||
modal
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const modalElement = (
|
||||
<ConfirmModal
|
||||
key={id}
|
||||
visible={true}
|
||||
onClose={destroy}
|
||||
{...config}
|
||||
type={type}
|
||||
/>
|
||||
);
|
||||
|
||||
setModals((prev) => [...prev, modalElement]);
|
||||
|
||||
return { destroy, update };
|
||||
};
|
||||
|
||||
const modalMethods = {
|
||||
info: (props: ConfirmModalProps) => createModal(props, "info"),
|
||||
success: (props: ConfirmModalProps) => createModal(props, "success"),
|
||||
error: (props: ConfirmModalProps) => createModal(props, "error"),
|
||||
warning: (props: ConfirmModalProps) => createModal(props, "warning"),
|
||||
confirm: (props: ConfirmModalProps) => createModal(props, "confirm"),
|
||||
};
|
||||
|
||||
const contextHolder = <>{modals}</>;
|
||||
|
||||
return [modalMethods, contextHolder];
|
||||
};
|
||||
|
||||
// Styles
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.45)",
|
||||
justifyContent: "flex-start",
|
||||
paddingTop: 100,
|
||||
alignItems: "center",
|
||||
},
|
||||
centered: {
|
||||
justifyContent: "center",
|
||||
paddingTop: 0,
|
||||
},
|
||||
noMask: {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
modal: {
|
||||
backgroundColor: "#fff",
|
||||
borderRadius: 8,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 12,
|
||||
elevation: 8,
|
||||
maxHeight: "90%",
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 24,
|
||||
paddingTop: 20,
|
||||
paddingBottom: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#f0f0f0",
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: "600",
|
||||
color: "#000",
|
||||
flex: 1,
|
||||
},
|
||||
closeButton: {
|
||||
padding: 4,
|
||||
marginLeft: 12,
|
||||
},
|
||||
body: {
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 20,
|
||||
},
|
||||
footer: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 24,
|
||||
paddingBottom: 20,
|
||||
paddingTop: 12,
|
||||
gap: 8,
|
||||
},
|
||||
button: {
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 6,
|
||||
minWidth: 70,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: 36,
|
||||
},
|
||||
cancelButton: {
|
||||
backgroundColor: "#fff",
|
||||
borderWidth: 1,
|
||||
borderColor: "#d9d9d9",
|
||||
},
|
||||
cancelButtonText: {
|
||||
color: "#000",
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
},
|
||||
okButton: {
|
||||
backgroundColor: "#1890ff",
|
||||
},
|
||||
primaryButton: {
|
||||
backgroundColor: "#1890ff",
|
||||
},
|
||||
okButtonText: {
|
||||
color: "#fff",
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
},
|
||||
disabledButton: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
confirmContent: {
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
confirmText: {
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
color: "#000",
|
||||
lineHeight: 22,
|
||||
},
|
||||
});
|
||||
|
||||
export default Modal;
|
||||
Reference in New Issue
Block a user