From 97af21912cc2c1e7ab1e9be2f25fc17130695e89 Mon Sep 17 00:00:00 2001 From: Djalma Araujo Date: Thu, 7 May 2026 10:26:55 -0300 Subject: [PATCH 1/2] [Bug Fix] Prevent Command dialog stacking (#230) --- .../controllers/ruby_ui/command_controller.js | 13 +++++++++++++ gem/lib/ruby_ui/command/command_controller.js | 13 +++++++++++++ gem/lib/ruby_ui/command/command_dialog_content.rb | 2 +- gem/test/ruby_ui/command_test.rb | 1 + 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/docs/app/javascript/controllers/ruby_ui/command_controller.js b/docs/app/javascript/controllers/ruby_ui/command_controller.js index 2ef0c47e9..0acf055dd 100644 --- a/docs/app/javascript/controllers/ruby_ui/command_controller.js +++ b/docs/app/javascript/controllers/ruby_ui/command_controller.js @@ -1,6 +1,8 @@ import { Controller } from "@hotwired/stimulus"; import Fuse from "fuse.js"; +const OPEN_DIALOG_SELECTOR = "[data-ruby-ui--command-dialog]"; + // Connects to data-controller="ruby-ui--command" export default class extends Controller { static targets = ["input", "group", "item", "empty", "content"]; @@ -37,6 +39,12 @@ export default class extends Controller { return; } + const openDialog = document.querySelector(OPEN_DIALOG_SELECTOR); + if (openDialog) { + this.focusDialogInput(openDialog); + return; + } + document.body.insertAdjacentHTML("beforeend", this.contentTarget.innerHTML); // prevent scroll on body document.body.classList.add("overflow-hidden"); @@ -144,4 +152,9 @@ export default class extends Controller { this.itemTargets.forEach((item) => this.toggleAriaSelected(item, false)); this.selectedIndex = -1; } + + focusDialogInput(dialog) { + const input = dialog.querySelector("[data-ruby-ui--command-target='input']"); + input?.focus(); + } } diff --git a/gem/lib/ruby_ui/command/command_controller.js b/gem/lib/ruby_ui/command/command_controller.js index 2ef0c47e9..0acf055dd 100644 --- a/gem/lib/ruby_ui/command/command_controller.js +++ b/gem/lib/ruby_ui/command/command_controller.js @@ -1,6 +1,8 @@ import { Controller } from "@hotwired/stimulus"; import Fuse from "fuse.js"; +const OPEN_DIALOG_SELECTOR = "[data-ruby-ui--command-dialog]"; + // Connects to data-controller="ruby-ui--command" export default class extends Controller { static targets = ["input", "group", "item", "empty", "content"]; @@ -37,6 +39,12 @@ export default class extends Controller { return; } + const openDialog = document.querySelector(OPEN_DIALOG_SELECTOR); + if (openDialog) { + this.focusDialogInput(openDialog); + return; + } + document.body.insertAdjacentHTML("beforeend", this.contentTarget.innerHTML); // prevent scroll on body document.body.classList.add("overflow-hidden"); @@ -144,4 +152,9 @@ export default class extends Controller { this.itemTargets.forEach((item) => this.toggleAriaSelected(item, false)); this.selectedIndex = -1; } + + focusDialogInput(dialog) { + const input = dialog.querySelector("[data-ruby-ui--command-target='input']"); + input?.focus(); + } } diff --git a/gem/lib/ruby_ui/command/command_dialog_content.rb b/gem/lib/ruby_ui/command/command_dialog_content.rb index 5ada024f7..d180cb006 100644 --- a/gem/lib/ruby_ui/command/command_dialog_content.rb +++ b/gem/lib/ruby_ui/command/command_dialog_content.rb @@ -18,7 +18,7 @@ def initialize(size: :md, **attrs) def view_template(&block) template(data: {ruby_ui__command_target: "content"}) do - div(data: {controller: "ruby-ui--command"}) do + div(data: {controller: "ruby-ui--command", ruby_ui__command_dialog: true}) do backdrop div(**attrs, &block) end diff --git a/gem/test/ruby_ui/command_test.rb b/gem/test/ruby_ui/command_test.rb index 1a4011a97..d8743b295 100644 --- a/gem/test/ruby_ui/command_test.rb +++ b/gem/test/ruby_ui/command_test.rb @@ -60,5 +60,6 @@ def test_render_with_all_items end assert_match(/Search/, output) + assert_match(/data-ruby-ui--command-dialog/, output) end end From dda391d851b71a8820ae2c6b82247b168c788626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Djalma=20Ara=C3=BAjo?= Date: Fri, 8 May 2026 09:58:40 -0300 Subject: [PATCH 2/2] docs(command): note single-instance dialog behavior --- docs/app/views/docs/command.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/app/views/docs/command.rb b/docs/app/views/docs/command.rb index 37ce24cba..8049c0f4e 100644 --- a/docs/app/views/docs/command.rb +++ b/docs/app/views/docs/command.rb @@ -93,6 +93,12 @@ def view_template RUBY end + Heading(level: 2) { "Single instance" } + + p(class: "text-muted-foreground") do + plain "The Command dialog is single-instance. Activating a trigger while the dialog is already open refocuses the existing dialog instead of stacking another one on top, so repeated keybindings or trigger clicks behave predictably." + end + render Components::ComponentSetup::Tabs.new(component_name: component) render Docs::ComponentsTable.new(component_files(component))