diff --git a/docs/app/javascript/controllers/ruby_ui/command_controller.js b/docs/app/javascript/controllers/ruby_ui/command_controller.js index 2ef0c47e..0acf055d 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/docs/app/views/docs/command.rb b/docs/app/views/docs/command.rb index 37ce24cb..8049c0f4 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)) diff --git a/gem/lib/ruby_ui/command/command_controller.js b/gem/lib/ruby_ui/command/command_controller.js index 2ef0c47e..0acf055d 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 5ada024f..d180cb00 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 1a4011a9..d8743b29 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