Skip to content
Merged
Show file tree
Hide file tree
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
26 changes: 7 additions & 19 deletions gem/lib/ruby_ui/dialog/dialog_content.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,20 @@ def initialize(size: :md, **attrs)
end

def view_template
template(data: {ruby_ui__dialog_target: "content"}) do
div(data_controller: "ruby-ui--dialog") do
backdrop
div(**attrs) do
yield
close_button
end
end
dialog(**attrs) do
yield
close_button
end
end

private

def default_attrs
{
data_state: "open",
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 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:rounded-lg md:w-full",
"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",
SIZES[@size]
]
}
Expand All @@ -43,7 +39,7 @@ def default_attrs
def close_button
button(
type: "button",
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 data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",
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",
data_action: "click->ruby-ui--dialog#dismiss"
) do
svg(
Expand All @@ -65,13 +61,5 @@ def close_button
span(class: "sr-only") { "Close" }
end
end

def backdrop
div(
data_state: "open",
data_action: "click->ruby-ui--dialog#dismiss esc->ruby-ui--dialog#dismiss",
class: "fixed pointer-events-auto inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=open]:fade-in-0"
)
end
end
end
32 changes: 22 additions & 10 deletions gem/lib/ruby_ui/dialog/dialog_controller.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
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")
}
}
88 changes: 88 additions & 0 deletions gem/test/ruby_ui/dialog_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,92 @@ def test_render_with_all_items

assert_match(/Open Dialog/, output)
end

# Regression test for #343: Dialog content must use native <dialog> element, not <div>
def test_dialog_content_renders_native_dialog_element
output = phlex do
RubyUI.Dialog do
RubyUI.DialogContent { "Content" }
end
end

assert_match(/<dialog[\s>]/, output, "DialogContent must render a native <dialog> element")
refute_match(/<template[\s>]/, output, "DialogContent must not use a <template> element")
end

def test_dialog_wrapper_renders_as_div_with_stimulus_controller
output = phlex do
RubyUI.Dialog do
RubyUI.DialogContent { "Content" }
end
end

assert_match(/data-controller="ruby-ui--dialog"/, output)
assert_match(/<div[^>]*data-controller="ruby-ui--dialog"/, output, "Dialog wrapper must be a <div>")
end

def test_dialog_content_has_stimulus_target
output = phlex do
RubyUI.Dialog do
RubyUI.DialogContent { "Content" }
end
end

assert_match(/data-ruby-ui--dialog-target="dialog"/, output)
end

def test_dialog_content_has_backdrop_click_action
output = phlex do
RubyUI.Dialog do
RubyUI.DialogContent { "Content" }
end
end

assert_match(/data-action="click->ruby-ui--dialog#backdropClick"/, output)
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
RubyUI.Dialog do
RubyUI.DialogContent(size: size) { "Content" }
end
end

assert_match(/#{Regexp.escape(expected_class)}/, output, "Size #{size} should apply class #{expected_class}")
end
end

def test_dialog_open_value_is_set_on_wrapper
output = phlex do
RubyUI.Dialog(open: true) do
RubyUI.DialogContent { "Content" }
end
end

assert_match(/data-ruby-ui--dialog-open-value/, output)
end

def test_close_button_has_dismiss_action
output = phlex do
RubyUI.Dialog do
RubyUI.DialogContent { "Content" }
end
end

assert_match(/data-action="click->ruby-ui--dialog#dismiss"/, output)
end

def test_trigger_has_open_action
output = phlex do
RubyUI.Dialog do
RubyUI.DialogTrigger do
RubyUI.Button { "Open" }
end
RubyUI.DialogContent { "Content" }
end
end

assert_match(/data-action="click->ruby-ui--dialog#open"/, output)
end
end
4 changes: 2 additions & 2 deletions mcp/data/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -1253,11 +1253,11 @@
},
{
"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 template(data: {ruby_ui__dialog_target: \"content\"}) do\n div(data_controller: \"ruby-ui--dialog\") do\n backdrop\n div(**attrs) do\n yield\n close_button\n end\n end\n end\n end\n\n private\n\n def default_attrs\n {\n data_state: \"open\",\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 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=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 data-[state=open]:bg-accent data-[state=open]:text-muted-foreground\",\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\n def backdrop\n div(\n data_state: \"open\",\n data_action: \"click->ruby-ui--dialog#dismiss esc->ruby-ui--dialog#dismiss\",\n class: \"fixed pointer-events-auto inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=open]:fade-in-0\"\n )\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 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",
"content": "import { Controller } from \"@hotwired/stimulus\"\n\n// Connects to data-controller=\"dialog\"\nexport default class extends Controller {\n static targets = [\"content\"]\n static values = {\n open: {\n type: Boolean,\n default: false\n },\n }\n\n connect() {\n if (this.openValue) {\n this.open()\n }\n }\n\n open(e) {\n e?.preventDefault();\n document.body.insertAdjacentHTML('beforeend', this.contentTarget.innerHTML)\n // prevent scroll on body\n document.body.classList.add('overflow-hidden')\n }\n\n dismiss() {\n // allow scroll on body\n document.body.classList.remove('overflow-hidden')\n // remove the element\n this.element.remove()\n }\n}\n"
"content": "import { Controller } from \"@hotwired/stimulus\"\n\n// Connects to data-controller=\"ruby-ui--dialog\"\nexport default class extends Controller {\n static targets = [\"dialog\"]\n static values = {\n open: {\n type: Boolean,\n default: false\n },\n }\n\n connect() {\n this.dialogTarget.addEventListener(\"close\", this.handleClose)\n if (this.openValue) {\n this.open()\n }\n }\n\n disconnect() {\n this.dialogTarget.removeEventListener(\"close\", this.handleClose)\n document.body.classList.remove(\"overflow-hidden\")\n }\n\n open(e) {\n e?.preventDefault()\n this.dialogTarget.showModal()\n document.body.classList.add(\"overflow-hidden\")\n }\n\n dismiss() {\n this.dialogTarget.close()\n }\n\n backdropClick(e) {\n if (e.target === this.dialogTarget) {\n this.dismiss()\n }\n }\n\n handleClose = () => {\n document.body.classList.remove(\"overflow-hidden\")\n }\n}\n"
},
{
"path": "dialog_description.rb",
Expand Down