From c05e8e07c2651ade10d6db7db566d032c93be481 Mon Sep 17 00:00:00 2001 From: Claudio Ortolina Date: Thu, 5 Feb 2026 14:56:04 +0000 Subject: [PATCH] Record picker keyboard navigation --- assets/js/app.js | 2 + assets/js/hooks/record-picker-navigation.js | 113 ++++++++++++++++++ .../live/record_set_live/record_picker.ex | 10 +- 3 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 assets/js/hooks/record-picker-navigation.js diff --git a/assets/js/app.js b/assets/js/app.js index 89c2e250..fb152a58 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -25,6 +25,7 @@ import topbar from "../vendor/topbar"; import { Hooks as FluxonHooks, DOM as FluxonDOM } from "fluxon"; import FormatNumberHook from "./hooks/format-number"; import UniversalSearchNavigationHook from "./hooks/universal-search-navigation"; +import RecordPickerNavigationHook from "./hooks/record-picker-navigation"; import confetti from "canvas-confetti"; import { createLiveToastHook } from "live_toast"; import banner from "./banner"; @@ -32,6 +33,7 @@ import banner from "./banner"; let Hooks = FluxonHooks; Hooks.FormatNumber = FormatNumberHook; Hooks.UniversalSearchNavigation = UniversalSearchNavigationHook; +Hooks.RecordPickerNavigation = RecordPickerNavigationHook; Hooks.LiveToast = createLiveToastHook(); const csrfToken = document diff --git a/assets/js/hooks/record-picker-navigation.js b/assets/js/hooks/record-picker-navigation.js new file mode 100644 index 00000000..54da56c6 --- /dev/null +++ b/assets/js/hooks/record-picker-navigation.js @@ -0,0 +1,113 @@ +export default { + mounted() { + this.selectedIndex = -1; // -1 means search input is focused + + this.keydownHandler = (event) => { + const modal = document.getElementById("record-picker-modal"); + if (!modal) return; + + switch (event.key) { + case "ArrowDown": + event.preventDefault(); + this.navigateDown(); + break; + case "ArrowUp": + event.preventDefault(); + this.navigateUp(); + break; + case "Enter": + if (this.selectedIndex >= 0) { + event.preventDefault(); + this.selectCurrentResult(); + } + break; + } + }; + + document.addEventListener("keydown", this.keydownHandler); + }, + + updated() { + // Reset selection when results change + this.selectedIndex = -1; + this.updateSelection(); + }, + + destroyed() { + if (this.keydownHandler) { + document.removeEventListener("keydown", this.keydownHandler); + } + }, + + navigateDown() { + const results = this.getResultElements(); + + if (results.length === 0) return; + + if (this.selectedIndex < results.length - 1) { + this.selectedIndex++; + } else { + this.selectedIndex = -1; + } + + this.updateSelection(); + }, + + navigateUp() { + const results = this.getResultElements(); + + if (results.length === 0) return; + + if (this.selectedIndex > -1) { + this.selectedIndex--; + } else { + this.selectedIndex = results.length - 1; + } + + this.updateSelection(); + }, + + selectCurrentResult() { + const results = this.getResultElements(); + + if (this.selectedIndex >= 0 && this.selectedIndex < results.length) { + results[this.selectedIndex].click(); + + if (this.selectedIndex === 0) { + this.navigateDown(); + } else { + this.naigateUp(); + } + } + }, + + updateSelection() { + const results = this.getResultElements(); + + results.forEach((result) => { + result.removeAttribute("aria-selected"); + }); + + if (this.selectedIndex === -1) { + const searchInput = document.getElementById("record-picker-search-input"); + if (searchInput) { + searchInput.focus(); + } + } else if (this.selectedIndex >= 0 && this.selectedIndex < results.length) { + const selectedResult = results[this.selectedIndex]; + selectedResult.setAttribute("aria-selected", "true"); + + selectedResult.scrollIntoView({ + block: "nearest", + behavior: "smooth" + }); + } + }, + + getResultElements() { + const container = this.el; + if (!container) return []; + + return Array.from(container.querySelectorAll('[role="option"]')); + } +}; diff --git a/lib/music_library_web/live/record_set_live/record_picker.ex b/lib/music_library_web/live/record_set_live/record_picker.ex index 02c95891..1384d8ae 100644 --- a/lib/music_library_web/live/record_set_live/record_picker.ex +++ b/lib/music_library_web/live/record_set_live/record_picker.ex @@ -7,7 +7,7 @@ defmodule MusicLibraryWeb.RecordSetLive.RecordPicker do @impl true def render(assigns) do ~H""" -
+

{@title} @@ -26,7 +26,7 @@ defmodule MusicLibraryWeb.RecordSetLive.RecordPicker do <.input type="search" size="sm" - id={:query} + id="record-picker-search-input" name={:query} value={@query} placeholder={gettext("Search")} @@ -48,7 +48,11 @@ defmodule MusicLibraryWeb.RecordSetLive.RecordPicker do