"use client"; import cx from "classnames"; import React, { useEffect, useMemo } from "react"; import ConditionalWrapper from "../../utils/ConditionalWrapper"; import { useStatic } from "../../utils/hooks"; import { Button } from "../Button"; import { Buttons } from "../Buttons"; import ModalStatic from "./Modal.static"; import { ModalBody } from "./ModalBody"; import { ModalCloseButton } from "./ModalCloseButton"; import { ModalTitle } from "./ModalTitle"; const CLASS_ROOT = "modal"; export const sizes = ["small", "large"] as const; export type ModalSize = (typeof sizes)[number]; interface ModalProps extends React.HTMLAttributes { /** Action buttons in footer */ actions?: React.ReactNode[]; /** Full height on XS */ fullHeight?: boolean; /** Sticky footer is visible when modal content is too long */ hasStickyFooter?: boolean; /** Html id attribute. */ id: string; /** isActive controls if modal is opened or closed */ isActive?: boolean; /** Callback fired when modal is closed */ onHide?: () => void; /** Custom header renderer. Receives id as function param. Returned element(s) must contain a header with close button. */ renderHeader?: (id: string) => React.ReactNode; /** Custom body renderer. Receives title as function param. Returned element(s) must contain a title. */ renderBody?: () => React.ReactNode; /** Custom footer renderer. Receives actions as function param. Returned element(s) must contain the actions. */ renderFooter?: () => React.ReactNode; /** Custom main title renderer. Receives id as function param. Returned element must be a heading and use this id for accessibility purposes */ renderTitle?: (id: string) => React.ReactNode; /** Modal dialog size */ size?: ModalSize; /** Main title */ title?: string; /** Child elements */ children?: React.ReactNode; /** Additional CSS classes */ className?: string; /** Disables header paddings */ disableHeaderSpacing?: boolean; /** Disables footer paddings */ disableFooterSpacing?: boolean; } const defaultProps = { actions: [ , ], }; const Modal: React.FC = ({ actions = defaultProps.actions, className, children, hasStickyFooter, id, isActive, onHide, size, title, renderHeader, renderBody, renderTitle, renderFooter, fullHeight, disableHeaderSpacing, disableFooterSpacing, ...other }) => { // In React controlled mode (using isActive), we skip data-modal // so vanilla JS won't initialize and move modal to #root-modals // onHide alone works in uncontrolled mode - it's just a callback const isReactControlled = isActive !== undefined; const modalConfig = useMemo( () => (isReactControlled ? { disablePortal: true } : {}), [isReactControlled], ); const [modalRef, instance] = useStatic(ModalStatic, modalConfig); useEffect(() => { if (!instance.current || !onHide) return; const handleHide = () => { onHide(); }; instance.current.instance.on("hide", handleHide); return () => { instance.current?.instance.off("hide", handleHide); }; }, [instance, onHide]); useEffect(() => { if (!instance.current) return; if (isActive) { instance.current.show(); } else if (isActive === false) { instance.current.hide(); } }, [instance, isActive]); const dialogClasses = cx( `${CLASS_ROOT}__dialog`, { [`${CLASS_ROOT}__dialog--${size}`]: size, [`${CLASS_ROOT}__dialog--full-height`]: fullHeight, }, className, ); const titleId = `${id}-title`; const dialogTitle = renderTitle ? ( renderTitle(id) ) : ( {title} ); return (