2022-05-29 20:48:51 +00:00
|
|
|
import { h, ComponentChildren } from 'preact';
|
2022-06-01 00:41:24 +00:00
|
|
|
import { createPortal } from 'preact/compat';
|
2022-05-10 13:52:06 +00:00
|
|
|
import { useCallback } from 'preact/hooks';
|
|
|
|
import { useHotKey } from './hooks/useHotKey';
|
|
|
|
|
2022-06-03 22:30:39 +00:00
|
|
|
import styles from './css/Modal.module.css';
|
2022-05-10 13:52:06 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* ModalOverlay creates a semi-transparent overlay in which the
|
|
|
|
* modal is centered.
|
|
|
|
*
|
|
|
|
* @returns ModalOverlay component
|
|
|
|
*/
|
2022-05-29 20:48:51 +00:00
|
|
|
export const ModalOverlay = ({ children }: { children: ComponentChildren }) => {
|
2022-05-10 13:52:06 +00:00
|
|
|
return (
|
2022-06-03 22:30:39 +00:00
|
|
|
<div className={styles.modalOverlay}>
|
2022-05-10 13:52:06 +00:00
|
|
|
{children}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ModalContent provides a styled container for modal content
|
|
|
|
*
|
|
|
|
* @returns ModalContent component
|
|
|
|
*/
|
2022-05-29 20:48:51 +00:00
|
|
|
export const ModalContent = ({ children }: { children: ComponentChildren }) => {
|
2022-05-10 13:52:06 +00:00
|
|
|
return (
|
2022-06-03 22:30:39 +00:00
|
|
|
<div className={styles.modalContent}>
|
2022-05-10 13:52:06 +00:00
|
|
|
{children}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ModalFooter provides a right-aligned container for modal buttons.
|
|
|
|
*
|
|
|
|
* @returns ModalFooter component
|
|
|
|
*/
|
2022-05-29 20:48:51 +00:00
|
|
|
export const ModalFooter = ({ children }: { children: ComponentChildren }) => {
|
2022-05-10 13:52:06 +00:00
|
|
|
return (
|
2022-06-03 22:30:39 +00:00
|
|
|
<footer className={styles.modalFooter}>
|
2022-05-10 13:52:06 +00:00
|
|
|
{children}
|
2022-06-01 00:41:24 +00:00
|
|
|
</footer>
|
2022-05-10 13:52:06 +00:00
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ModalCloseButton component properties
|
|
|
|
*/
|
|
|
|
interface ModalCloseButtonProp {
|
|
|
|
onClose: (closeBox?: boolean) => void;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Renders a close button and registers a global Escape key
|
|
|
|
* hook to trigger it.
|
|
|
|
*
|
|
|
|
* @param onClose Close callback
|
|
|
|
* @returns ModalClose component
|
|
|
|
*/
|
|
|
|
export const ModalCloseButton = ({ onClose }: ModalCloseButtonProp) => {
|
|
|
|
const doClose = useCallback(() => onClose(true), [onClose]);
|
|
|
|
useHotKey('Escape', doClose);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<button onClick={doClose} title="Close">
|
|
|
|
{'\u2715'}
|
|
|
|
</button>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2022-05-12 00:20:49 +00:00
|
|
|
type OnCloseCallback = (closeBox?: boolean) => void;
|
|
|
|
|
2022-05-10 13:52:06 +00:00
|
|
|
/**
|
|
|
|
* ModalHeader component properties
|
|
|
|
*/
|
2022-05-12 00:20:49 +00:00
|
|
|
|
2022-05-10 13:52:06 +00:00
|
|
|
export interface ModalHeaderProps {
|
2022-05-12 00:20:49 +00:00
|
|
|
onClose?: OnCloseCallback;
|
2022-05-10 13:52:06 +00:00
|
|
|
title: string;
|
2022-06-01 00:41:24 +00:00
|
|
|
icon?: string;
|
2022-05-10 13:52:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Header used internally for Modal component
|
|
|
|
*
|
|
|
|
* @param onClose Close callback
|
|
|
|
* @param title Modal title
|
|
|
|
* @returns ModalHeader component
|
|
|
|
*/
|
2022-06-01 00:41:24 +00:00
|
|
|
export const ModalHeader = ({ onClose, title, icon }: ModalHeaderProps) => {
|
2022-05-10 13:52:06 +00:00
|
|
|
return (
|
2022-06-03 22:30:39 +00:00
|
|
|
<header className={styles.modalHeader}>
|
|
|
|
<span className={styles.modalTitle}>
|
|
|
|
{icon && <i className={`fa-solid fa-${icon}`} role="img" />}
|
2022-06-01 00:41:24 +00:00
|
|
|
{' '}
|
|
|
|
{title}
|
|
|
|
</span>
|
2022-05-10 13:52:06 +00:00
|
|
|
{onClose && <ModalCloseButton onClose={onClose} />}
|
2022-06-01 00:41:24 +00:00
|
|
|
</header>
|
2022-05-10 13:52:06 +00:00
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Modal component properties
|
|
|
|
*/
|
|
|
|
export interface ModalProps {
|
2022-05-10 15:04:20 +00:00
|
|
|
onClose?: (closeBox?: boolean) => void;
|
|
|
|
isOpen: boolean;
|
|
|
|
title: string;
|
2022-05-29 20:48:51 +00:00
|
|
|
children: ComponentChildren;
|
2022-06-01 00:41:24 +00:00
|
|
|
icon?: string;
|
2022-05-10 13:52:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Very simple modal component, provides a transparent overlay, title bar
|
|
|
|
* with optional close box if onClose is provided. ModalContent and
|
|
|
|
* ModalFooter components are provided for convenience but not required.
|
|
|
|
*
|
|
|
|
* @param isOpen true to show modal
|
|
|
|
* @param title Modal title
|
2022-05-12 00:20:49 +00:00
|
|
|
* @param onClose Close callback
|
2022-05-10 13:52:06 +00:00
|
|
|
* @returns Modal component
|
|
|
|
*/
|
2022-05-29 20:48:51 +00:00
|
|
|
export const Modal = ({
|
2022-05-10 13:52:06 +00:00
|
|
|
isOpen,
|
|
|
|
children,
|
2022-05-12 00:20:49 +00:00
|
|
|
title,
|
|
|
|
...props
|
2022-05-29 20:48:51 +00:00
|
|
|
}: ModalProps) => {
|
2022-05-10 13:52:06 +00:00
|
|
|
return (
|
2022-06-01 00:41:24 +00:00
|
|
|
isOpen ? createPortal((
|
2022-05-10 13:52:06 +00:00
|
|
|
<ModalOverlay>
|
2022-06-03 22:30:39 +00:00
|
|
|
<div className={styles.modal} role="dialog">
|
2022-05-12 00:20:49 +00:00
|
|
|
{title && <ModalHeader title={title} {...props} />}
|
2022-05-10 13:52:06 +00:00
|
|
|
{children}
|
|
|
|
</div>
|
|
|
|
</ModalOverlay>
|
2022-06-01 00:41:24 +00:00
|
|
|
), document.body) : null
|
2022-05-10 13:52:06 +00:00
|
|
|
);
|
|
|
|
};
|