diff --git a/app/javascript/controllers/navigable_list_controller.js b/app/javascript/controllers/navigable_list_controller.js new file mode 100644 index 000000000..d6bf52a0f --- /dev/null +++ b/app/javascript/controllers/navigable_list_controller.js @@ -0,0 +1,74 @@ +import { Controller } from "@hotwired/stimulus" +import { nextFrame } from "helpers/timing_helpers" + +export default class extends Controller { + static targets = [ "item" ] + static values = { selectionAttribute: { type: String, default: "aria-current" } } + + // Actions + + navigate(event) { + if (this.itemTargets.includes(event.target)) { + this.#keyHandlers[event.key]?.call(this, event) + } + } + + selectLast(event) { + this.#setCurrentFrom(this.itemTargets[this.itemTargets.length - 1]) + } + + #handleArrowKey(event, fn) { + if (event.shiftKey || event.metaKey || event.ctrlKey) { return } + fn.call() + event.preventDefault() + } + + #selectPrevious() { + if (this.currentItem.previousElementSibling) { + this.#setCurrentFrom(this.currentItem.previousElementSibling) + } + } + + #selectNext() { + if (this.currentItem.nextElementSibling) { + this.#setCurrentFrom(this.currentItem.nextElementSibling) + } + } + + async #setCurrentFrom(element) { + const selectedItem = this.itemTargets.find(item => item.contains(element)) + + if (selectedItem) { + this.#clearSelection() + selectedItem.setAttribute(this.selectionAttributeValue, "true") + this.currentItem = selectedItem + await nextFrame() + this.currentItem.focus() + } + } + + #clearSelection() { + for (const item of this.itemTargets) { + item.removeAttribute(this.selectionAttributeValue) + } + } + + #keyHandlers = { + ArrowDown(event) { + this.#handleArrowKey(event, this.#selectNext.bind(this)) + }, + ArrowUp(event) { + this.#handleArrowKey(event, this.#selectPrevious.bind(this)) + }, + ArrowRight(event) { + this.#handleArrowKey(event, this.#selectNext.bind(this)) + }, + ArrowLeft(event) { + this.#handleArrowKey(event, this.#selectPrevious.bind(this)) + }, + Enter(event) { + // this.#currentFirstFocusableControl?.focus() + // event.preventDefault() + } + } +} diff --git a/app/views/commands/_command.html.erb b/app/views/commands/_command.html.erb index 6cfb53375..7261472ba 100644 --- a/app/views/commands/_command.html.erb +++ b/app/views/commands/_command.html.erb @@ -1,4 +1,6 @@ -
  • +<%= tag.li class: "min-width full-width flex gap-half terminal__item justify-start", tabindex: 0, data: { + action: "keydown.enter->terminal#restoreCommand:π", + navigable_list_target: "item" } do %> <%= button_tag command.title, type: "button", class: "btn btn--plain overflow-ellipsis terminal__command flex-item-grow justify-start", data: { action: "toggle-class#remove terminal#restoreCommand", line: command.line } %> @@ -6,4 +8,4 @@ <%= button_to "Undo", command_undo_path(command), class: "btn btn--plain terminal__button flex-item-justify-end", data: { turbo_confirm: "Are you sure you want to undo '#{command.title}'?", turbo_frame: "_top" } %> <% end %> -
  • +<% end %>