Skip to content

[Bug Fix] Overlays: release Command scroll lock on teardown, settle Tooltip exit on cancel - #535

Open
djalmaaraujo wants to merge 2 commits into
mainfrom
djalma1/overlay-disconnect-cleanup
Open

djalmaaraujo wants to merge 2 commits into
mainfrom
djalma1/overlay-disconnect-cleanup

Conversation

@djalmaaraujo

@djalmaaraujo djalmaaraujo commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #506. That PR moved every overlay's hide behind the exit animation. Two overlays still leak on an interrupted/torn-down close.

CommandDialog — body stays scroll-locked after teardown

show() adds overflow-hidden to <body>; it is removed only in afterExit(). When the dialog is destroyed without a dismiss() (Turbo navigation, turbo:morph on a persisted body), disconnect()settleExit(panel) sees data-state="open" and returns early, so afterExit() never runs and the page stays unscrollable.

Fix: disconnect() releases the lock unconditionally, independent of exit state.

Tooltip — exit run cut short orphans the cloned node

Tooltip mounts a clone on <body> and removes it on animationend. It never listened for animationcancel, so an exit animation interrupted by a backgrounded tab or a competing style change leaves the clone in the DOM forever. The other overlays already listen for both events; this brings Tooltip in line. The existing animationName === "exit" + data-state === "closed" guards already do the right thing for the cancel case.

How to test

CommandDialog scroll lock

  1. Open a Command dialog (<body> gains overflow-hidden).
  2. Navigate away via a Turbo link (or trigger a morph) while it is open, instead of closing it.
  3. Before: the next page cannot scroll — overflow-hidden is stuck on <body>. After: the page scrolls normally.

Tooltip orphan

  1. Hover a tooltip trigger to show it, then move away to start the exit animation.
  2. Interrupt the exit (switch tabs mid-fade, or remove the trigger).
  3. Before: a stray tooltip node remains under document.body. After: it is removed.

Both are JS-only lifecycle fixes; mcp/data/registry.json is regenerated from the two controllers.


Summary by cubic

Fixes CommandDialog leaving <body> scroll-locked when Turbo navigation or morph tears it down mid-open, and fixes Tooltip leaving a cloned node behind when its exit animation is cancelled.

Bug Fixes

  • CommandDialog releases the overflow-hidden scroll lock on disconnect() only when the panel is still open; the normal close path already releases it in afterExit().
  • Tooltip now listens for animationcancel in addition to animationend, so a cut-short exit removes the cloned node.
  • Both fixes are JS-only; mcp/data/registry.json is regenerated from the two controllers.

Written for commit 93f5af5. Summary will update on new commits.

Review in cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="gem/lib/ruby_ui/command/command_controller.js">

<violation number="1" location="gem/lib/ruby_ui/command/command_controller.js:23">
P2: When another overlay is still open, this teardown clears its shared `overflow-hidden` lock and re-enables page scrolling. Use a shared reference-counted lock, or release the lock only when no other overlay still owns it.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

disconnect() {
// Torn down without dismiss() (Turbo nav/morph) never runs afterExit, which is
// the only place the <body> scroll lock is released. Release it here regardless.
document.body.classList.remove("overflow-hidden");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When another overlay is still open, this teardown clears its shared overflow-hidden lock and re-enables page scrolling. Use a shared reference-counted lock, or release the lock only when no other overlay still owns it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At gem/lib/ruby_ui/command/command_controller.js, line 23:

<comment>When another overlay is still open, this teardown clears its shared `overflow-hidden` lock and re-enables page scrolling. Use a shared reference-counted lock, or release the lock only when no other overlay still owns it.</comment>

<file context>
@@ -18,6 +18,9 @@ export default class extends Controller {
   disconnect() {
+    // Torn down without dismiss() (Turbo nav/morph) never runs afterExit, which is
+    // the only place the <body> scroll lock is released. Release it here regardless.
+    document.body.classList.remove("overflow-hidden");
     // Nothing is left to wait for the exit animation, so apply the pending removal now.
     if (this.hasPanelTarget) this.settleExit(this.panelTarget);
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid, but pre-existing and systemic rather than introduced here: dialog, alert_dialog, command and command_dialog all toggle the same overflow-hidden body class and every one already removes it unconditionally on its own close, so closing any one while another is open unlocks scroll on main today. A ref-counted lock is the right fix, but it spans those four controllers and changes their normal-close behaviour — out of scope for this teardown-leak follow-up, worth its own PR. I have narrowed the added removal to fire only when the panel is still open (the actual torn-down-mid-open leak), so it no longer touches the lock on a normal-close disconnect.

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