Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 59 additions & 4 deletions packages/cyberstorm/src/newComponents/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import {
isValidElement,
type PropsWithChildren,
type ReactNode,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import "./Modal.css";
import { NewButton, NewIcon } from "../..";
Expand Down Expand Up @@ -189,11 +192,63 @@ export function Modal(props: ModalProps) {
titleContent,
footerContent,
ariaDescribedby,
contentClasses,
open: controlledOpen,
onOpenChange,
defaultOpen,
} = props;

const filteredChildren: ReactNode[] = [];
const contentRef = useRef<HTMLDivElement | null>(null);

const isControlled = controlledOpen !== undefined;
const [uncontrolledOpen, setUncontrolledOpen] = useState<boolean>(
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]
);
Comment on lines +226 to +241
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Simplify after fixing state management.

Once internal state management is removed (per previous comment), lines 225-227 become unnecessary:

  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]
+    [onOpenChange, restoreBodyPointerEvents]
  );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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]
);
const handleOpenChange = useCallback(
(next: boolean) => {
onOpenChange?.(next);
if (!next) {
if (typeof window !== "undefined") {
window.requestAnimationFrame(restoreBodyPointerEvents);
} else {
restoreBodyPointerEvents();
}
}
},
[onOpenChange, restoreBodyPointerEvents]
);
🤖 Prompt for AI Agents
In packages/cyberstorm/src/newComponents/Modal/Modal.tsx around lines 222-237,
remove the now-unnecessary internal state update block (the if (!isControlled) {
setUncontrolledOpen(next); } lines) as internal state management was removed;
then update the handleOpenChange dependencies to drop isControlled (and any
setter) so the useCallback deps include only onOpenChange and
restoreBodyPointerEvents, leaving the function to call onOpenChange and handle
restoreBodyPointerEvents when closing.


useEffect(() => {
if (!effectiveOpen) {
restoreBodyPointerEvents();
}
return () => {
restoreBodyPointerEvents();
};
}, [effectiveOpen, restoreBodyPointerEvents]);

let exit = <Modal.Exit />;

let title = titleContent ? <Modal.Title>{titleContent}</Modal.Title> : null;
Expand Down Expand Up @@ -238,9 +293,9 @@ export function Modal(props: ModalProps) {

return (
<Dialog.Root
open={props.open}
onOpenChange={props.onOpenChange}
defaultOpen={props.defaultOpen}
{...(isControlled ? { open: controlledOpen } : {})}
{...(defaultOpen !== undefined ? { defaultOpen } : {})}
Comment on lines +296 to +297
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Passing both open and defaultOpen in controlled mode

The component passes defaultOpen to Dialog.Root even when in controlled mode. This violates React's controlled/uncontrolled component pattern - a component should never receive both open (controlled) and defaultOpen (uncontrolled) props simultaneously.

If a user provides both open={true} and defaultOpen={false}, both will be passed to Dialog.Root, causing unpredictable behavior.

Fix:

{...(isControlled ? { open: controlledOpen } : {})}
{...(!isControlled && defaultOpen !== undefined ? { defaultOpen } : {})}

This ensures defaultOpen is only passed in uncontrolled mode.

Suggested change
{...(isControlled ? { open: controlledOpen } : {})}
{...(defaultOpen !== undefined ? { defaultOpen } : {})}
{...(isControlled ? { open: controlledOpen } : {})}
{...(!isControlled && defaultOpen !== undefined ? { defaultOpen } : {})}

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

onOpenChange={handleOpenChange}
>
{trigger ? <Dialog.Trigger asChild>{trigger}</Dialog.Trigger> : null}
<Dialog.Portal>
Expand All @@ -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) => {
Expand Down
Loading