132 lines
No EOL
3.4 KiB
TypeScript
132 lines
No EOL
3.4 KiB
TypeScript
import React from 'react';
|
||
import styles from './Popup.module.css';
|
||
|
||
// Action button interface
|
||
export interface PopupAction {
|
||
label: string;
|
||
icon?: string | React.ReactElement;
|
||
onClick: () => void;
|
||
disabled?: boolean;
|
||
loading?: boolean;
|
||
variant?: 'primary' | 'secondary' | 'success' | 'danger';
|
||
}
|
||
|
||
// Generic popup props
|
||
export interface PopupProps {
|
||
isOpen: boolean;
|
||
title: string;
|
||
onClose: () => void;
|
||
children: React.ReactNode;
|
||
footerContent?: React.ReactNode;
|
||
className?: string;
|
||
size?: 'small' | 'medium' | 'large' | 'fullscreen';
|
||
closable?: boolean;
|
||
actions?: PopupAction[];
|
||
}
|
||
|
||
// Generic Popup component - can be used for any content
|
||
export function Popup({
|
||
isOpen,
|
||
title,
|
||
onClose,
|
||
children,
|
||
footerContent,
|
||
className = '',
|
||
size = 'medium',
|
||
closable = true,
|
||
actions = []
|
||
}: PopupProps) {
|
||
|
||
// Handle escape key
|
||
React.useEffect(() => {
|
||
const handleEscape = (e: KeyboardEvent) => {
|
||
if (e.key === 'Escape' && closable) {
|
||
onClose();
|
||
}
|
||
};
|
||
|
||
if (isOpen) {
|
||
document.addEventListener('keydown', handleEscape);
|
||
// Prevent body scroll when popup is open
|
||
document.body.style.overflow = 'hidden';
|
||
}
|
||
|
||
return () => {
|
||
document.removeEventListener('keydown', handleEscape);
|
||
document.body.style.overflow = 'unset';
|
||
};
|
||
}, [isOpen, closable, onClose]);
|
||
|
||
if (!isOpen) return null;
|
||
|
||
// Handle backdrop click
|
||
const handleBackdropClick = (e: React.MouseEvent) => {
|
||
if (e.target === e.currentTarget && closable) {
|
||
onClose();
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className={styles.overlay} onClick={handleBackdropClick}>
|
||
<div className={`${styles.popup} ${styles[size]} ${className}`}>
|
||
{/* Header */}
|
||
<div className={styles.header}>
|
||
<h2 className={styles.title}>{title}</h2>
|
||
<div className={styles.headerActions}>
|
||
{/* Action buttons */}
|
||
{actions.map((action, index) => (
|
||
<button
|
||
key={index}
|
||
className={`${styles.actionButton} ${styles[action.variant || 'primary']}`}
|
||
onClick={action.onClick}
|
||
disabled={action.disabled || action.loading}
|
||
title={action.label}
|
||
>
|
||
{action.loading ? (
|
||
<>
|
||
<span className={styles.spinner}></span>
|
||
{action.label}
|
||
</>
|
||
) : (
|
||
<>
|
||
{action.icon && (
|
||
<span style={{ fontSize: '18px' }}>
|
||
{typeof action.icon === 'string' ? action.icon : action.icon}
|
||
</span>
|
||
)}
|
||
{action.label}
|
||
</>
|
||
)}
|
||
</button>
|
||
))}
|
||
|
||
{/* Close button */}
|
||
{closable && (
|
||
<button
|
||
className={styles.closeButton}
|
||
onClick={onClose}
|
||
aria-label="Close"
|
||
>
|
||
×
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Content */}
|
||
<div className={styles.content}>
|
||
{children}
|
||
</div>
|
||
|
||
{/* Footer (optional) */}
|
||
{footerContent && (
|
||
<div className={styles.footer}>
|
||
{footerContent}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default Popup;
|