import { useEffect, useRef } from "preact/hooks"; import { t } from "../../services/i18n"; import { ComponentChildren } from "preact"; import type { CSSProperties, RefObject } from "preact/compat"; interface ModalProps { className: string; title: string | ComponentChildren; size: "xl" | "lg" | "md" | "sm"; children: ComponentChildren; /** * Items to display in the modal header, apart from the title itself which is handled separately. */ header?: ComponentChildren; footer?: ComponentChildren; footerStyle?: CSSProperties; footerAlignment?: "right" | "between"; minWidth?: string; maxWidth?: number; zIndex?: number; /** * If true, the modal body will be scrollable if the content overflows. * This is useful for larger modals where you want to keep the header and footer visible * while allowing the body content to scroll. * Defaults to false. */ scrollable?: boolean; /** * If set, the modal body and footer will be wrapped in a form and the submit event will call this function. * Especially useful for user input that can be submitted with Enter key. */ onSubmit?: () => void; /** Called when the modal is shown. */ onShown?: () => void; /** Called when the modal is hidden, either via close button, backdrop click or submit. */ onHidden?: () => void; helpPageId?: string; /** * Gives access to the underlying modal element. This is useful for manipulating the modal directly * or for attaching event listeners. */ modalRef?: RefObject; /** * Gives access to the underlying form element of the modal. This is only set if `onSubmit` is provided. */ formRef?: RefObject; bodyStyle?: CSSProperties; } export default function Modal({ children, className, size, title, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden: onHidden, modalRef: _modalRef, formRef: _formRef, bodyStyle }: ModalProps) { const modalRef = _modalRef ?? useRef(null); const formRef = _formRef ?? useRef(null); if (onShown || onHidden) { useEffect(() => { const modalElement = modalRef.current; if (!modalElement) { return; } if (onShown) { modalElement.addEventListener("shown.bs.modal", onShown); } if (onHidden) { modalElement.addEventListener("hidden.bs.modal", onHidden); } return () => { if (onShown) { modalElement.removeEventListener("shown.bs.modal", onShown); } if (onHidden) { modalElement.removeEventListener("hidden.bs.modal", onHidden); } }; }, [ ]); } const dialogStyle: CSSProperties = {}; if (zIndex) { dialogStyle.zIndex = zIndex; } const documentStyle: CSSProperties = {}; if (maxWidth) { documentStyle.maxWidth = `${maxWidth}px`; } if (minWidth) { documentStyle.minWidth = minWidth; } return (
{!title || typeof title === "string" ? (
{title ?? <> }
) : ( title )} {header} {helpPageId && ( )}
{onSubmit ? (
{ e.preventDefault(); onSubmit(); }}> {children}
) : ( {children} )}
); } function ModalInner({ children, footer, footerAlignment, bodyStyle, footerStyle: _footerStyle }: Pick) { const footerStyle: CSSProperties = _footerStyle ?? {}; if (footerAlignment === "between") { footerStyle.justifyContent = "space-between"; } return ( <>
{children}
{footer && (
{footer}
)} ); }