From c205045e8fbc551b4da702c929e7ad18c1557692 Mon Sep 17 00:00:00 2001 From: Oksamies Date: Thu, 13 Nov 2025 10:28:15 +0200 Subject: [PATCH] Enhance Modal component to support controlled and uncontrolled states with improved open state management --- .../src/newComponents/Modal/Modal.tsx | 63 +++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/packages/cyberstorm/src/newComponents/Modal/Modal.tsx b/packages/cyberstorm/src/newComponents/Modal/Modal.tsx index baccdb5d6..8cdf8df5f 100644 --- a/packages/cyberstorm/src/newComponents/Modal/Modal.tsx +++ b/packages/cyberstorm/src/newComponents/Modal/Modal.tsx @@ -3,7 +3,10 @@ import { isValidElement, type PropsWithChildren, type ReactNode, + useCallback, + useEffect, useRef, + useState, } from "react"; import "./Modal.css"; import { NewButton, NewIcon } from "../.."; @@ -189,11 +192,63 @@ export function Modal(props: ModalProps) { titleContent, footerContent, ariaDescribedby, + contentClasses, + open: controlledOpen, + onOpenChange, + defaultOpen, } = props; const filteredChildren: ReactNode[] = []; const contentRef = useRef(null); + const isControlled = controlledOpen !== undefined; + const [uncontrolledOpen, setUncontrolledOpen] = useState( + defaultOpen ?? false + ); + + let effectiveOpen = uncontrolledOpen; + if (isControlled) { + effectiveOpen = controlledOpen!; + } + + // Radix Select/DropdownMenu can leave body pointer-events disabled if the modal closes first. + const restoreBodyPointerEvents = useCallback(() => { + if (typeof document === "undefined") return; + const bodyStyle = document.body.style; + if (bodyStyle.pointerEvents === "none") { + bodyStyle.pointerEvents = ""; + if (typeof CustomEvent === "function") { + document.dispatchEvent(new CustomEvent("dismissableLayer.update")); + } + } + }, []); + + const handleOpenChange = useCallback( + (next: boolean) => { + onOpenChange?.(next); + if (!isControlled) { + setUncontrolledOpen(next); + } + if (!next) { + if (typeof window !== "undefined") { + window.requestAnimationFrame(restoreBodyPointerEvents); + } else { + restoreBodyPointerEvents(); + } + } + }, + [onOpenChange, isControlled, restoreBodyPointerEvents] + ); + + useEffect(() => { + if (!effectiveOpen) { + restoreBodyPointerEvents(); + } + return () => { + restoreBodyPointerEvents(); + }; + }, [effectiveOpen, restoreBodyPointerEvents]); + let exit = ; let title = titleContent ? {titleContent} : null; @@ -238,9 +293,9 @@ export function Modal(props: ModalProps) { return ( {trigger ? {trigger} : null} @@ -250,7 +305,7 @@ export function Modal(props: ModalProps) { "modal", "modal__content", ...componentClasses("modal", csVariant, csSize, undefined), - props.contentClasses + contentClasses )} aria-describedby={ariaDescribedby} onOpenAutoFocus={(e) => {