diff --git a/docs/app/javascript/controllers/index.js b/docs/app/javascript/controllers/index.js index e68815bd..8857f204 100644 --- a/docs/app/javascript/controllers/index.js +++ b/docs/app/javascript/controllers/index.js @@ -40,6 +40,9 @@ application.register("ruby-ui--combobox", RubyUi__ComboboxController) import RubyUi__CommandController from "./ruby_ui/command_controller" application.register("ruby-ui--command", RubyUi__CommandController) +import RubyUi__CommandDialogController from "./ruby_ui/command_dialog_controller" +application.register("ruby-ui--command-dialog", RubyUi__CommandDialogController) + import RubyUi__ContextMenuController from "./ruby_ui/context_menu_controller" application.register("ruby-ui--context-menu", RubyUi__ContextMenuController) diff --git a/docs/app/javascript/controllers/ruby_ui/command_controller.js b/docs/app/javascript/controllers/ruby_ui/command_controller.js index 2ef0c47e..91b80495 100644 --- a/docs/app/javascript/controllers/ruby_ui/command_controller.js +++ b/docs/app/javascript/controllers/ruby_ui/command_controller.js @@ -3,14 +3,7 @@ import Fuse from "fuse.js"; // Connects to data-controller="ruby-ui--command" export default class extends Controller { - static targets = ["input", "group", "item", "empty", "content"]; - - static values = { - open: { - type: Boolean, - default: false, - }, - }; + static targets = ["input", "group", "item", "empty"]; connect() { this.selectedIndex = -1; @@ -22,24 +15,6 @@ export default class extends Controller { this.inputTarget.focus(); this.searchIndex = this.buildSearchIndex(); this.toggleVisibility(this.emptyTargets, false); - - if (this.openValue && this.hasContentTarget) { - this.open(); - } - } - - open(e) { - if (e) { - e.preventDefault(); - } - - if (!this.hasContentTarget) { - return; - } - - document.body.insertAdjacentHTML("beforeend", this.contentTarget.innerHTML); - // prevent scroll on body - document.body.classList.add("overflow-hidden"); } dismiss() { @@ -49,6 +24,10 @@ export default class extends Controller { this.element.remove(); } + focusInput() { + this.inputTarget?.focus(); + } + filter(e) { // Deselect any previously selected item this.deselectAll(); diff --git a/docs/app/javascript/controllers/ruby_ui/command_dialog_controller.js b/docs/app/javascript/controllers/ruby_ui/command_dialog_controller.js new file mode 100644 index 00000000..7763a79e --- /dev/null +++ b/docs/app/javascript/controllers/ruby_ui/command_dialog_controller.js @@ -0,0 +1,34 @@ +import { Controller } from "@hotwired/stimulus"; + +// Connects to data-controller="ruby-ui--command-dialog" +export default class extends Controller { + static targets = ["content"]; + static outlets = ["ruby-ui--command"]; + + rubyUiCommandOutletConnected(controller) { + this.openOutlet = controller; + } + + rubyUiCommandOutletDisconnected() { + this.openOutlet = null; + } + + open(e) { + if (e) { + e.preventDefault(); + } + + if (!this.hasContentTarget) { + return; + } + + if (this.openOutlet) { + this.openOutlet.focusInput(); + return; + } + + document.body.insertAdjacentHTML("beforeend", this.contentTarget.innerHTML); + // prevent scroll on body + document.body.classList.add("overflow-hidden"); + } +} 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..91b80495 100644 --- a/gem/lib/ruby_ui/command/command_controller.js +++ b/gem/lib/ruby_ui/command/command_controller.js @@ -3,14 +3,7 @@ import Fuse from "fuse.js"; // Connects to data-controller="ruby-ui--command" export default class extends Controller { - static targets = ["input", "group", "item", "empty", "content"]; - - static values = { - open: { - type: Boolean, - default: false, - }, - }; + static targets = ["input", "group", "item", "empty"]; connect() { this.selectedIndex = -1; @@ -22,24 +15,6 @@ export default class extends Controller { this.inputTarget.focus(); this.searchIndex = this.buildSearchIndex(); this.toggleVisibility(this.emptyTargets, false); - - if (this.openValue && this.hasContentTarget) { - this.open(); - } - } - - open(e) { - if (e) { - e.preventDefault(); - } - - if (!this.hasContentTarget) { - return; - } - - document.body.insertAdjacentHTML("beforeend", this.contentTarget.innerHTML); - // prevent scroll on body - document.body.classList.add("overflow-hidden"); } dismiss() { @@ -49,6 +24,10 @@ export default class extends Controller { this.element.remove(); } + focusInput() { + this.inputTarget?.focus(); + } + filter(e) { // Deselect any previously selected item this.deselectAll(); diff --git a/gem/lib/ruby_ui/command/command_dialog.rb b/gem/lib/ruby_ui/command/command_dialog.rb index c6a4b41f..b8f59d50 100644 --- a/gem/lib/ruby_ui/command/command_dialog.rb +++ b/gem/lib/ruby_ui/command/command_dialog.rb @@ -10,7 +10,10 @@ def view_template(&) def default_attrs { - data: {controller: "ruby-ui--command"} + data: { + controller: "ruby-ui--command-dialog", + ruby_ui__command_dialog_ruby_ui__command_outlet: "[data-ruby-ui--command-dialog-instance]" + } } end end diff --git a/gem/lib/ruby_ui/command/command_dialog_content.rb b/gem/lib/ruby_ui/command/command_dialog_content.rb index 5ada024f..0781b6e4 100644 --- a/gem/lib/ruby_ui/command/command_dialog_content.rb +++ b/gem/lib/ruby_ui/command/command_dialog_content.rb @@ -17,8 +17,8 @@ def initialize(size: :md, **attrs) end def view_template(&block) - template(data: {ruby_ui__command_target: "content"}) do - div(data: {controller: "ruby-ui--command"}) do + template(data: {ruby_ui__command_dialog_target: "content"}) do + div(data: {controller: "ruby-ui--command", ruby_ui__command_dialog_instance: true}) do backdrop div(**attrs, &block) end diff --git a/gem/lib/ruby_ui/command/command_dialog_controller.js b/gem/lib/ruby_ui/command/command_dialog_controller.js new file mode 100644 index 00000000..7763a79e --- /dev/null +++ b/gem/lib/ruby_ui/command/command_dialog_controller.js @@ -0,0 +1,34 @@ +import { Controller } from "@hotwired/stimulus"; + +// Connects to data-controller="ruby-ui--command-dialog" +export default class extends Controller { + static targets = ["content"]; + static outlets = ["ruby-ui--command"]; + + rubyUiCommandOutletConnected(controller) { + this.openOutlet = controller; + } + + rubyUiCommandOutletDisconnected() { + this.openOutlet = null; + } + + open(e) { + if (e) { + e.preventDefault(); + } + + if (!this.hasContentTarget) { + return; + } + + if (this.openOutlet) { + this.openOutlet.focusInput(); + return; + } + + document.body.insertAdjacentHTML("beforeend", this.contentTarget.innerHTML); + // prevent scroll on body + document.body.classList.add("overflow-hidden"); + } +} diff --git a/gem/lib/ruby_ui/command/command_dialog_trigger.rb b/gem/lib/ruby_ui/command/command_dialog_trigger.rb index 9c12eb66..d3c5858c 100644 --- a/gem/lib/ruby_ui/command/command_dialog_trigger.rb +++ b/gem/lib/ruby_ui/command/command_dialog_trigger.rb @@ -8,7 +8,7 @@ class CommandDialogTrigger < Base ].freeze def initialize(keybindings: DEFAULT_KEYBINDINGS, **attrs) - @keybindings = keybindings.map { |kb| "#{kb}->ruby-ui--command#open" } + @keybindings = keybindings.map { |kb| "#{kb}->ruby-ui--command-dialog#open" } super(**attrs) end @@ -21,7 +21,7 @@ def view_template(&) def default_attrs { data: { - action: ["click->ruby-ui--command#open", @keybindings.join(" ")] + action: ["click->ruby-ui--command-dialog#open", @keybindings.join(" ")] } } end diff --git a/gem/test/ruby_ui/command_test.rb b/gem/test/ruby_ui/command_test.rb index 1a4011a9..63323082 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-controller="ruby-ui--command"/, output) end end