Skip to content

Conversation

@Oksamies
Copy link
Contributor

No description provided.

Copy link
Contributor Author

@coderabbitai
Copy link

coderabbitai bot commented Nov 13, 2025

Walkthrough

The Modal component now supports both controlled and uncontrolled open state via an isControlled flag, an internal uncontrolledOpen state, and an effectiveOpen derived value. All open changes are routed through handleOpenChange, which coordinates calls to onOpenChange, updates internal state, and restores body pointer-events via restoreBodyPointerEvents. A useEffect restores body pointer-events on open changes and unmount. Dialog.Root is conditionally passed open/defaultOpen based on control mode. The public prop contentClasses?: string was added to ModalProps.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to evaluate relevance to the changeset. Add a description explaining the motivation, implementation details, and any breaking changes or migration guidance for users.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: enhancing the Modal component to support both controlled and uncontrolled states with improved open state management.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 11-13-enhance_modal_component_to_support_controlled_and_uncontrolled_states_with_improved_open_state_management

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ed8b2e0 and c205045.

📒 Files selected for processing (1)
  • packages/cyberstorm/src/newComponents/Modal/Modal.tsx (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cyberstorm/src/newComponents/Modal/Modal.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build
  • GitHub Check: Generate visual diffs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 06e8ed7 and ed8b2e0.

📒 Files selected for processing (1)
  • packages/cyberstorm/src/newComponents/Modal/Modal.tsx (4 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build
  • GitHub Check: Generate visual diffs
🔇 Additional comments (1)
packages/cyberstorm/src/newComponents/Modal/Modal.tsx (1)

210-220: LGTM—reasonable workaround for Radix layer stacking.

The SSR guard and conditional restoration logic are sound. The custom event dispatch may help re-sync Radix's dismissable layer tracking.

Comment on lines +222 to +241
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]
);
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.

Comment on lines 239 to 246
useEffect(() => {
if (!open) {
restoreBodyPointerEvents();
}
return () => {
restoreBodyPointerEvents();
};
}, [open, restoreBodyPointerEvents]);
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

Redundant restoration on close.

When the modal closes, handleOpenChange already calls restoreBodyPointerEvents (line 230), then this effect fires again because open changed. The unmount cleanup is valuable, but the !open branch duplicates work.

  useEffect(() => {
-    if (!open) {
-      restoreBodyPointerEvents();
-    }
    return () => {
      restoreBodyPointerEvents();
    };
-  }, [open, restoreBodyPointerEvents]);
+  }, [restoreBodyPointerEvents]);

Now you only restore on unmount, avoiding the double-call when closing.

📝 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
useEffect(() => {
if (!open) {
restoreBodyPointerEvents();
}
return () => {
restoreBodyPointerEvents();
};
}, [open, restoreBodyPointerEvents]);
useEffect(() => {
return () => {
restoreBodyPointerEvents();
};
}, [restoreBodyPointerEvents]);
🤖 Prompt for AI Agents
In packages/cyberstorm/src/newComponents/Modal/Modal.tsx around lines 239 to
246, the useEffect redundantly calls restoreBodyPointerEvents when open becomes
false (duplicate of the call in handleOpenChange at ~line 230); remove the if
(!open) branch so the effect only returns a cleanup that calls
restoreBodyPointerEvents on unmount, and update the effect dependency array to
only include restoreBodyPointerEvents to prevent it running on every open
change.

@codecov
Copy link

codecov bot commented Nov 13, 2025

Codecov Report

❌ Patch coverage is 0% with 50 lines in your changes missing coverage. Please review.
✅ Project coverage is 9.84%. Comparing base (86d570d) to head (c205045).
⚠️ Report is 7 commits behind head on master.

Files with missing lines Patch % Lines
...kages/cyberstorm/src/newComponents/Modal/Modal.tsx 0.00% 50 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##           master   #1609      +/-   ##
=========================================
- Coverage    9.86%   9.84%   -0.03%     
=========================================
  Files         309     309              
  Lines       22555   22601      +46     
  Branches      405     405              
=========================================
  Hits         2224    2224              
- Misses      20331   20377      +46     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Oksamies Oksamies force-pushed the 11-13-enhance_modal_component_to_support_controlled_and_uncontrolled_states_with_improved_open_state_management branch from ed8b2e0 to c205045 Compare November 13, 2025 08:53
Comment on lines +296 to +297
{...(isControlled ? { open: controlledOpen } : {})}
{...(defaultOpen !== undefined ? { defaultOpen } : {})}
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants