Skip to content

[Bug Fix] Dialog: only dismiss on clicks outside the panel (#570) - #580

Merged
cirdes merged 2 commits into
mainfrom
fix/dialog-backdrop-click-570
Oct 30, 2026
Merged

cirdes merged 2 commits into
mainfrom
fix/dialog-backdrop-click-570

Conversation

@jacksonpires

@jacksonpires jacksonpires commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Closes #570

Problem

DialogContent is the native <dialog> itself, with p-6 gap-4. backdropClick dismissed on any click whose target was the <dialog>, but clicks on the panel's own padding and on the gaps between children report the same target as a click on ::backdrop. So clicking inside the panel, between two fields, closed the dialog.

A related case: selecting text inside the dialog and releasing the mouse outside it also fires click on the <dialog>, which closed it mid-selection.

Fix

  • dialog_controller.js: a new backdropPointerdown records whether the press started outside the panel's rect. backdropClick dismisses only when the press both started and ended outside it.
  • dialog_content.rb: data-action now also wires pointerdown->ruby-ui--dialog#backdropPointerdown.
  • mcp/data/registry.json rebuilt (rake mcp:build).

No markup or API change, so docs/app/views/docs/dialog.rb is unchanged. I kept the panel as the <dialog> instead of moving to a shadcn-style inner wrapper, which would change the DOM that users customize.

Testing

  • cd gem && bundle exec rake: 298 runs, 0 failures. standardrb: no offenses. Added a regression test for the pointerdown action.
  • Browser: the dialog was rendered by the gem and driven with real CDP mouse events in headless Chrome, against main's controller and this branch's:
Scenario main This PR
Click on panel padding closes (bug) stays open
Click in gap between inputs closes (bug) stays open
Select text, drag outward, release closes (bug) stays open
Press on backdrop, release inside n/a stays open
Click on backdrop closes closes
dialog-570-before-after

To test manually: open any Dialog on the docs site, click in the padding or between two fields (it should stay open), then click the backdrop (it should close).

Note

#517 (exit animation) also edits backdropClick. Whichever PR merges second gets a small conflict in that one method.

🤖 Generated with Claude Code


Summary by cubic

Brings the dialog to shadcn Base UI + vega parity and fixes clicks on the panel's own padding and gaps dismissing it — those report the <dialog> as target, same as backdrop clicks, so a press now counts as a backdrop click only when it both starts and ends outside the panel's rect. Dragged text selections that release outside no longer close the dialog either.

  • Vega classes for the panel, ::backdrop overlay, header, footer, and title (now <h2>); explicit size: is unchanged and the default width is now max-w-md at sm+.
  • DialogContent(show_close_button:) toggles the corner close button; DialogFooter(show_close_button:) adds a footer Close button.
  • Open and close animate off data-state; Esc, backdrop, and the close buttons play the exit animation before close(), with motion-reduce support.
  • The controller names the dialog from its title/description (aria-labelledby/aria-describedby) and sets aria-haspopup/aria-controls/aria-expanded on triggers.

Written for commit 33b055e. Summary will update on new commits.

View guided diff Turn on auto-fix

The panel's padding and the gaps between its children report the <dialog>
itself as the click target, same as the ::backdrop, so clicking them closed
the dialog. Dismiss only when the press both starts and ends outside the
panel's rect, which also keeps a text selection dragged outward from
closing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jacksonpires jacksonpires self-assigned this Oct 28, 2026
@jacksonpires
jacksonpires requested a review from cirdes as a code owner October 28, 2026 17:50

@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.

No issues found across 4 files

View guided diff | Turn on auto-fix | Re-trigger cubic

@cirdes

cirdes commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator

shadcn audit

Repo ruby-ui/ruby_ui
PR #580
RubyUI base f11510881090dabcc260817a3227f41f4a6527e6
RubyUI head ead314545dfa1a6fc46a03c8e782989d1362f907
shadcn 96da9cc7f1aa10580a3741664e2a77c721206c00
⚠️ Environment  Font: shadcn sandbox loads the "Inter Variable" webfont; the RubyUI preview declares Inter, system-ui, ... and loads no webfont (falls back to a locally installed Inter). Title/body font-size and weight differences are class differences, not this.
⚠️ Environment  Viewport: window resize did not apply on the shadcn tab (855x929), so both tabs were emulated at 1280x800 @2x.
❌ Theme  10 token diffs vs shadcn neutral
     light --chart-1: rubyui oklch(0.646 0.222 41.116)  shadcn oklch(0.87 0 0)
     light --chart-2: rubyui oklch(0.6 0.118 184.704)  shadcn oklch(0.556 0 0)
     light --chart-3: rubyui oklch(0.398 0.07 227.392)  shadcn oklch(0.439 0 0)
     light --chart-4: rubyui oklch(0.828 0.189 84.429)  shadcn oklch(0.371 0 0)
     light --chart-5: rubyui oklch(0.769 0.188 70.08)  shadcn oklch(0.269 0 0)
     dark --chart-1: rubyui oklch(0.488 0.243 264.376)  shadcn oklch(0.87 0 0)
     dark --chart-2: rubyui oklch(0.696 0.17 162.48)  shadcn oklch(0.556 0 0)
     dark --chart-3: rubyui oklch(0.769 0.188 70.08)  shadcn oklch(0.439 0 0)
     dark --chart-4: rubyui oklch(0.627 0.265 303.9)  shadcn oklch(0.371 0 0)
     dark --chart-5: rubyui oklch(0.645 0.246 16.439)  shadcn oklch(0.269 0 0)
Dialog  (ref: bases/base/ui/dialog.tsx + style-vega.css @ 96da9cc7)
  ❌ Look       backdrop:bg-background/80 backdrop:backdrop-blur-sm (computed oklab(1 0 0 / 0.8) light, oklab(0.145 0 0 / 0.8) dark, blur 8px)
               vega: bg-black/10 supports-backdrop-filter:backdrop-blur-xs (computed oklab(0 0 0 / 0.1), blur 4px)
               Overlay: RubyUI styles the native ::backdrop; shadcn renders a dialog-overlay div.
               ↳ style-vega.css:463
  ❌ Look       flex flex-col gap-4 border bg-background shadow-lg sm:rounded-lg max-w-lg duration-200 (512px wide, radius 10px, bg oklch(0.145 0 0) dark)
               vega: grid gap-6 ring-1 ring-foreground/10 bg-popover text-popover-foreground rounded-xl text-sm sm:max-w-md duration-100 (448px wide, radius 14px, bg oklch(0.205 0 0) dark)
               Content panel. left-[50%]/top-[50%]/translate-[-50%] compute the same as left-1/2/top-1/2/-translate-1/2 (640px/400px, -50% -50%).
               ↳ style-vega.css:467
  ❌ Look       no text color on DialogContent: the <dialog> keeps the UA color CanvasText, computed rgb(0, 0, 0) in dark mode, so the title, body text, Cancel label and close icon render black on oklch(0.145 0 0)
               vega: text-popover-foreground (computed oklch(0.985 0 0) in dark)
               Pre-existing, not introduced by this PR. Visible in screens/dialog-open-rubyui-dark.png. Description stays readable because it sets text-muted-foreground. Adding text-foreground (or text-popover-foreground) to DialogContent fixes it.
               ↳ style-vega.css:467
  ❌ Look       flex flex-col space-y-1.5 text-center sm:text-left rtl:sm:text-right
               vega: flex flex-col gap-2
               Header: 6px vs 8px between title and description.
               ↳ style-vega.css:475
  ❌ Look       text-lg font-semibold leading-none tracking-tight (18px / 600 / -0.45px)
               vega: font-heading leading-none font-medium (14px / 500)
               Title.
               ↳ style-vega.css:483
  ❌ Look       text-sm text-muted-foreground
               vega: text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground
               Description: plain text computes the same (14px, oklch(0.708 0 0) dark); only the link styling is missing.
               ↳ style-vega.css:487
  ✅ Look       flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 gap-y-2 sm:gap-y-0 rtl:space-x-reverse
               Footer: space-x-2 and gap-2 both give 8px between buttons at sm+, gap-y-2 = gap-2 below sm (checked with computed styles).
               ↳ style-vega.css:479
  ❌ Look       16x16 icon button: absolute end-4 top-4 rounded-sm opacity-70 focus:ring-2 focus:ring-ring focus:ring-offset-2
               vega: ghost icon Button size-8 (32x32) rounded-[min(var(--radius-md),10px)] hover:bg-muted absolute top-4 right-4, opacity 1
               Close button. Position matches (end-4 = right-4 in LTR, 16px/16px computed); size, radius, opacity and hover differ. Reference warns the classes come through <Button>; graded from the sandbox DOM.
               ↳ sandbox DOM + style-vega.css:471
  ✅ HTML       <dialog> + showModal() — intentional
               Native modal dialog: the browser provides inert, Esc and focus handling.
               ↳ audit/divergences.yml
  ❌ HTML       DialogTitle renders <h3>
               shadcn: DialogTitle renders <h2>
               Heading level differs.
               ↳ sandbox DOM, bases/base/ui/dialog.tsx:122
  ❌ HTML       DialogTrigger is a <div class="inline-block"> wrapping the button
               shadcn: the trigger is the <button> itself (render prop)
               Extra wrapper element around the trigger.
               ↳ sandbox DOM, bases/base/ui/dialog.tsx:14
  ❌ Props      no showCloseButton on DialogContent or DialogFooter (close button always rendered in DialogContent, never in DialogFooter)
               shadcn: DialogContent showCloseButton (default true), DialogFooter showCloseButton (default false)
               Dialog open -> open is mapped. RubyUI-only: DialogContent size (xs..full).
               ↳ bases/base/ui/dialog.tsx:42
  ⚠️ ARIA       <dialog> has no aria-labelledby/aria-describedby; no accessible name
               shadcn: aria-labelledby -> "Edit profile", aria-describedby -> "Make changes to your profile here."
               Base UI wires Title/Description ids on the popup; RubyUI doesn't.
               ↳ sandbox DOM, bases/base/ui/dialog.tsx:122
  ⚠️ ARIA       trigger button has no aria-haspopup, aria-expanded or aria-controls
               shadcn: aria-haspopup="dialog", aria-expanded true/false, aria-controls -> popup id
               Read on the [data-audit-trigger] element before and after opening.
               ↳ sandbox DOM
  ✅ Behavior   press on panel padding (top and left edge), on body text, drag body->backdrop, drag padding->backdrop, drag backdrop->padding: stays open; click on backdrop (corner and 6px outside the edge): closes
               This PR's fix. Coordinate cases used synthetic pointerdown/mousedown/pointerup/mouseup/click at elementFromPoint, with click sent to the common ancestor for drags (as Chrome does); identical script on both sides, identical results. A real click on the body text also kept RubyUI open.
               ↳ browser run (sandbox vs preview)
  ✅ Behavior   initial focus on Cancel; Esc closes and returns focus to the trigger; real click on Cancel closes and returns focus to the trigger
               Light and dark.
               ↳ browser run (sandbox vs preview)
  ✅ Behavior   Tab/Shift+Tab stay inside: Cancel -> Save changes -> Close, wrapping through the browser UI (native showModal)
               Page content behind is never reached on either side.
               ↳ browser run (sandbox vs preview)
  ✅ Behavior   body overflow: hidden while open (controller adds overflow-hidden), removed on close
               Scroll lock. window.scrollBy still moves both pages (overflow: hidden allows programmatic scroll), so both behave the same.
               ↳ browser run (sandbox vs preview)
  ⚠️ Animation  no exit animation: the dialog leaves at once (no animations running after close)
               shadcn: exit animation 100ms on dialog-content (fade-out-0 zoom-out-95) and dialog-overlay (fade-out-0)
               Native <dialog> needs @starting-style / transition-behavior: allow-discrete to animate out.
               ↳ style-vega.css:467
  ⚠️ Animation  enter: 200ms fade-in-0 zoom-in-95 on the dialog only; backdrop not animated
               shadcn: enter: 100ms on dialog-content (fade-in-0 zoom-in-95) and dialog-overlay (fade-in-0)
               ↳ style-vega.css:463
  ✅ Native     showModal(): inert page, Esc, initial focus and focus return from the browser; scroll lock kept by the controller — native
               Exit animation loss is graded on the Animation line.
               ↳ https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal
  ⚠️ Native     hand-rolled backdrop dismissal (pointerdown + click + rect check)
               shadcn: closedby="any" on <dialog> gives light dismiss natively
               Judgment call: closedby ships in Chrome 134 and Firefox 141 but Safari only in preview (MDN browser-compat-data), so the JS is still needed as a fallback. Consider closedby="any" plus this handler as a fallback once Safari ships it.
               ↳ https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog#closedby
  ❌ Spec       <dialog> has no accessible name (no aria-labelledby, aria-label or title)
               shadcn: dialog named by aria-labelledby -> "Edit profile"
               WAI-ARIA 1.2 dialog: Accessible Name Required = True; RubyUI's <dialog> has none, shadcn's has one.
               ↳ https://www.w3.org/TR/wai-aria-1.2/#dialog
  ✅ Spec       <dialog> opened with showModal() is exposed as modal (a11y tree: "dialog modal") — follows spec
               APG: "The dialog container element has aria-modal set to true." shadcn's popup lacks it; RubyUI's native modal dialog is modal by construction.
               ↳ https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/#wai-ariaroles,states,andproperties

* [Documentation] Plan: Dialog shadcn parity

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* [Bug Fix] Dialog: vega panel, overlay and text color

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* [Bug Fix] Dialog: vega header, title (h2), description, footer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* [Feature] Dialog: show_close_button on content and footer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* [Bug Fix] Dialog: animate open and close

Drive tw-animate enter/exit on the panel and its ::backdrop off
data-state, set by the controller. Close (X, Cancel, Esc, backdrop)
now flips data-state="closed", waits for the dialog's own animations
to finish, then calls close(). Reduced motion skips the animation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* [Bug Fix] Dialog: hold exit frame and ignore nested cancel events

Keep the closed state's last frame with fill-mode-forwards so the panel
and backdrop don't snap back before close(). Only handle `cancel` fired
on the dialog itself; a dismissed file picker inside it bubbles one too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* [Bug Fix] Dialog: name the dialog and expose trigger state

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* [Documentation] Dialog: close button options and default size

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* [Bug Fix] Dialog: final review fixes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* [Bug Fix] Dialog: close button interaction states

Match the vega ghost icon-sm Button the shadcn close button renders
through: transparent border that turns ring-colored on keyboard focus,
1px press offset, select-none, and disabled/aria-expanded states.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* [Documentation] Audit: register Dialog trigger wrapper divergence

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* [Bug Fix] Dialog: address review on plan text and close button tests

Plan: drop local paths, use end-4 and motion-reduce:animate-none! to match
the code. Tests: render the close button tests without nesting DialogContent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@cirdes
cirdes merged commit 8155df5 into main Oct 30, 2026
10 checks passed
@cirdes
cirdes deleted the fix/dialog-backdrop-click-570 branch October 30, 2026 11:49
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.

Dialog: clicking the panel's inner padding closes it (backdropClick matches the <dialog> itself)

2 participants