diff --git a/docs/app/javascript/controllers/ruby_ui/dialog_controller.js b/docs/app/javascript/controllers/ruby_ui/dialog_controller.js index 26bf1fa0..7b7e368c 100644 --- a/docs/app/javascript/controllers/ruby_ui/dialog_controller.js +++ b/docs/app/javascript/controllers/ruby_ui/dialog_controller.js @@ -1,8 +1,8 @@ import { Controller } from "@hotwired/stimulus" -// Connects to data-controller="dialog" +// Connects to data-controller="ruby-ui--dialog" export default class extends Controller { - static targets = ["content"] + static targets = ["dialog"] static values = { open: { type: Boolean, @@ -11,22 +11,34 @@ export default class extends Controller { } connect() { + this.dialogTarget.addEventListener("close", this.handleClose) if (this.openValue) { this.open() } } + disconnect() { + this.dialogTarget.removeEventListener("close", this.handleClose) + document.body.classList.remove("overflow-hidden") + } + open(e) { - e?.preventDefault(); - document.body.insertAdjacentHTML('beforeend', this.contentTarget.innerHTML) - // prevent scroll on body - document.body.classList.add('overflow-hidden') + e?.preventDefault() + this.dialogTarget.showModal() + document.body.classList.add("overflow-hidden") } dismiss() { - // allow scroll on body - document.body.classList.remove('overflow-hidden') - // remove the element - this.element.remove() + this.dialogTarget.close() + } + + backdropClick(e) { + if (e.target === this.dialogTarget) { + this.dismiss() + } + } + + handleClose = () => { + document.body.classList.remove("overflow-hidden") } } diff --git a/gem/lib/ruby_ui/dialog/dialog_content.rb b/gem/lib/ruby_ui/dialog/dialog_content.rb index 5ed38a0e..14450435 100644 --- a/gem/lib/ruby_ui/dialog/dialog_content.rb +++ b/gem/lib/ruby_ui/dialog/dialog_content.rb @@ -30,7 +30,7 @@ def default_attrs data_ruby_ui__dialog_target: "dialog", data_action: "click->ruby-ui--dialog#backdropClick", class: [ - "fixed flex flex-col pointer-events-auto left-[50%] top-[50%] z-50 w-full max-h-screen overflow-y-auto translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 backdrop:bg-background/80 backdrop:backdrop-blur-sm open:animate-in open:fade-in-0 open:zoom-in-95 sm:rounded-lg md:w-full", + "fixed open:flex flex-col pointer-events-auto left-[50%] top-[50%] z-50 w-full max-h-screen overflow-y-auto translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 backdrop:bg-background/80 backdrop:backdrop-blur-sm open:animate-in open:fade-in-0 open:zoom-in-95 sm:rounded-lg md:w-full", SIZES[@size] ] } diff --git a/gem/test/ruby_ui/dialog_test.rb b/gem/test/ruby_ui/dialog_test.rb index 19307ba0..90061d34 100644 --- a/gem/test/ruby_ui/dialog_test.rb +++ b/gem/test/ruby_ui/dialog_test.rb @@ -77,6 +77,21 @@ def test_dialog_content_has_backdrop_click_action assert_match(/data-action="click->ruby-ui--dialog#backdropClick"/, output) end + # Regression test: a closed native must stay hidden. The bare `flex` + # utility (author CSS) overrides the UA `dialog:not([open]) { display: none }`, + # making the dialog always visible. Display must be gated on the open: variant. + def test_dialog_content_does_not_force_display_when_closed + output = phlex do + RubyUI.Dialog do + RubyUI.DialogContent { "Content" } + end + end + + classes = output[/ to display; use `open:flex`" + assert_includes classes, "open:flex", "Dialog must apply flex only when open (open:flex)" + end + def test_dialog_content_sizes {xs: "max-w-sm", sm: "max-w-md", md: "max-w-lg", lg: "max-w-2xl", xl: "max-w-4xl", full: "max-w-full"}.each do |size, expected_class| output = phlex do diff --git a/mcp/data/registry.json b/mcp/data/registry.json index 1240aad2..384af86e 100644 --- a/mcp/data/registry.json +++ b/mcp/data/registry.json @@ -1329,7 +1329,7 @@ }, { "path": "dialog_content.rb", - "content": "# frozen_string_literal: true\n\nmodule RubyUI\n class DialogContent < Base\n SIZES = {\n xs: \"max-w-sm\",\n sm: \"max-w-md\",\n md: \"max-w-lg\",\n lg: \"max-w-2xl\",\n xl: \"max-w-4xl\",\n full: \"max-w-full\"\n }\n\n def initialize(size: :md, **attrs)\n @size = size\n super(**attrs)\n end\n\n def view_template\n dialog(**attrs) do\n yield\n close_button\n end\n end\n\n private\n\n def default_attrs\n {\n data_ruby_ui__dialog_target: \"dialog\",\n data_action: \"click->ruby-ui--dialog#backdropClick\",\n class: [\n \"fixed flex flex-col pointer-events-auto left-[50%] top-[50%] z-50 w-full max-h-screen overflow-y-auto translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 backdrop:bg-background/80 backdrop:backdrop-blur-sm open:animate-in open:fade-in-0 open:zoom-in-95 sm:rounded-lg md:w-full\",\n SIZES[@size]\n ]\n }\n end\n\n def close_button\n button(\n type: \"button\",\n class: \"absolute end-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none\",\n data_action: \"click->ruby-ui--dialog#dismiss\"\n ) do\n svg(\n width: \"15\",\n height: \"15\",\n viewbox: \"0 0 15 15\",\n fill: \"none\",\n xmlns: \"http://www.w3.org/2000/svg\",\n class: \"h-4 w-4\"\n ) do |s|\n s.path(\n d:\n \"M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z\",\n fill: \"currentColor\",\n fill_rule: \"evenodd\",\n clip_rule: \"evenodd\"\n )\n end\n span(class: \"sr-only\") { \"Close\" }\n end\n end\n end\nend\n" + "content": "# frozen_string_literal: true\n\nmodule RubyUI\n class DialogContent < Base\n SIZES = {\n xs: \"max-w-sm\",\n sm: \"max-w-md\",\n md: \"max-w-lg\",\n lg: \"max-w-2xl\",\n xl: \"max-w-4xl\",\n full: \"max-w-full\"\n }\n\n def initialize(size: :md, **attrs)\n @size = size\n super(**attrs)\n end\n\n def view_template\n dialog(**attrs) do\n yield\n close_button\n end\n end\n\n private\n\n def default_attrs\n {\n data_ruby_ui__dialog_target: \"dialog\",\n data_action: \"click->ruby-ui--dialog#backdropClick\",\n class: [\n \"fixed open:flex flex-col pointer-events-auto left-[50%] top-[50%] z-50 w-full max-h-screen overflow-y-auto translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 backdrop:bg-background/80 backdrop:backdrop-blur-sm open:animate-in open:fade-in-0 open:zoom-in-95 sm:rounded-lg md:w-full\",\n SIZES[@size]\n ]\n }\n end\n\n def close_button\n button(\n type: \"button\",\n class: \"absolute end-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none\",\n data_action: \"click->ruby-ui--dialog#dismiss\"\n ) do\n svg(\n width: \"15\",\n height: \"15\",\n viewbox: \"0 0 15 15\",\n fill: \"none\",\n xmlns: \"http://www.w3.org/2000/svg\",\n class: \"h-4 w-4\"\n ) do |s|\n s.path(\n d:\n \"M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z\",\n fill: \"currentColor\",\n fill_rule: \"evenodd\",\n clip_rule: \"evenodd\"\n )\n end\n span(class: \"sr-only\") { \"Close\" }\n end\n end\n end\nend\n" }, { "path": "dialog_controller.js",