diff --git a/lib/mv_web/live/group_live/show.ex b/lib/mv_web/live/group_live/show.ex index ad24110..9e5bdf6 100644 --- a/lib/mv_web/live/group_live/show.ex +++ b/lib/mv_web/live/group_live/show.ex @@ -23,6 +23,7 @@ defmodule MvWeb.GroupLive.Show do alias Mv.Membership alias MvWeb.Helpers.MemberHelpers, as: MemberHelpers + alias MvWeb.Live.MemberDropdownNav @impl true def mount(_params, _session, socket) do @@ -566,56 +567,8 @@ defmodule MvWeb.GroupLive.Show do end @impl true - def handle_event("member_dropdown_keydown", %{"key" => "ArrowDown"}, socket) do - return_if_dropdown_closed(socket, fn -> - max_index = length(socket.assigns.available_members) - 1 - current = socket.assigns.focused_member_index - - new_index = - case current do - nil -> 0 - index when index < max_index -> index + 1 - _ -> current - end - - {:noreply, assign(socket, focused_member_index: new_index)} - end) - end - - @impl true - def handle_event("member_dropdown_keydown", %{"key" => "ArrowUp"}, socket) do - return_if_dropdown_closed(socket, fn -> - current = socket.assigns.focused_member_index - - new_index = - case current do - nil -> 0 - 0 -> 0 - index -> index - 1 - end - - {:noreply, assign(socket, focused_member_index: new_index)} - end) - end - - @impl true - def handle_event("member_dropdown_keydown", %{"key" => "Enter"}, socket) do - return_if_dropdown_closed(socket, fn -> - select_focused_member(socket) - end) - end - - @impl true - def handle_event("member_dropdown_keydown", %{"key" => "Escape"}, socket) do - return_if_dropdown_closed(socket, fn -> - {:noreply, assign(socket, show_member_dropdown: false, focused_member_index: nil)} - end) - end - - @impl true - def handle_event("member_dropdown_keydown", _params, socket) do - # Ignore other keys - {:noreply, socket} + def handle_event("member_dropdown_keydown", params, socket) do + MemberDropdownNav.handle_keydown(params, socket, fn -> select_focused_member(socket) end) end @impl true @@ -705,14 +658,6 @@ defmodule MvWeb.GroupLive.Show do end # Helper functions - defp return_if_dropdown_closed(socket, fun) do - if socket.assigns.show_member_dropdown do - fun.() - else - {:noreply, socket} - end - end - defp select_focused_member(socket) do case socket.assigns.focused_member_index do nil -> diff --git a/lib/mv_web/live/member_dropdown_nav.ex b/lib/mv_web/live/member_dropdown_nav.ex new file mode 100644 index 0000000..1a5d4f2 --- /dev/null +++ b/lib/mv_web/live/member_dropdown_nav.ex @@ -0,0 +1,83 @@ +defmodule MvWeb.Live.MemberDropdownNav do + @moduledoc """ + Shared keyboard-navigation logic for the member-search dropdown used by the + user form and the group show LiveViews. + + Both views keep their own `member_dropdown_keydown` event entry point and their + own `select_focused_member/1` (the selection effect differs per view), but the + arrow/enter/escape navigation over `:focused_member_index` and the + dropdown-open guard are identical and live here. + + The caller passes a zero-arity `select_focused` callback that performs the + view-specific selection of the currently focused entry. + """ + + import Phoenix.Component, only: [assign: 2] + + @type socket :: Phoenix.LiveView.Socket.t() + @type result :: {:noreply, socket} + + @doc """ + Handles a `member_dropdown_keydown` event for the shared dropdown. + + Navigation keys move `:focused_member_index` within + `[0, length(available_members) - 1]`; Enter invokes the view-specific + `select_focused` callback; Escape closes the dropdown; any other key is a + no-op. All key handling is guarded so that keystrokes while the dropdown is + closed are ignored. + """ + @spec handle_keydown(map(), socket, (-> result)) :: result + def handle_keydown(%{"key" => "ArrowDown"}, socket, _select_focused) do + return_if_dropdown_closed(socket, fn -> + max_index = length(socket.assigns.available_members) - 1 + current = socket.assigns.focused_member_index + + new_index = + case current do + nil -> 0 + index when index < max_index -> index + 1 + _ -> current + end + + {:noreply, assign(socket, focused_member_index: new_index)} + end) + end + + def handle_keydown(%{"key" => "ArrowUp"}, socket, _select_focused) do + return_if_dropdown_closed(socket, fn -> + current = socket.assigns.focused_member_index + + new_index = + case current do + nil -> 0 + 0 -> 0 + index -> index - 1 + end + + {:noreply, assign(socket, focused_member_index: new_index)} + end) + end + + def handle_keydown(%{"key" => "Enter"}, socket, select_focused) do + return_if_dropdown_closed(socket, select_focused) + end + + def handle_keydown(%{"key" => "Escape"}, socket, _select_focused) do + return_if_dropdown_closed(socket, fn -> + {:noreply, assign(socket, show_member_dropdown: false, focused_member_index: nil)} + end) + end + + def handle_keydown(_params, socket, _select_focused) do + # Ignore other keys + {:noreply, socket} + end + + defp return_if_dropdown_closed(socket, func) do + if socket.assigns.show_member_dropdown do + func.() + else + {:noreply, socket} + end + end +end diff --git a/lib/mv_web/live/user_live/form.ex b/lib/mv_web/live/user_live/form.ex index 60763ab..297f1f6 100644 --- a/lib/mv_web/live/user_live/form.ex +++ b/lib/mv_web/live/user_live/form.ex @@ -43,6 +43,7 @@ defmodule MvWeb.UserLive.Form do alias Mv.Membership alias Mv.Membership.Member, as: MemberResource alias MvWeb.Helpers.MemberHelpers + alias MvWeb.Live.MemberDropdownNav import MvWeb.LiveHelpers, only: [current_actor: 1, submit_form: 3] import MvWeb.Authorization, only: [can?: 3] @@ -571,56 +572,8 @@ defmodule MvWeb.UserLive.Form do end @impl true - def handle_event("member_dropdown_keydown", %{"key" => "ArrowDown"}, socket) do - return_if_dropdown_closed(socket, fn -> - max_index = length(socket.assigns.available_members) - 1 - current = socket.assigns.focused_member_index - - new_index = - case current do - nil -> 0 - index when index < max_index -> index + 1 - _ -> current - end - - {:noreply, assign(socket, focused_member_index: new_index)} - end) - end - - @impl true - def handle_event("member_dropdown_keydown", %{"key" => "ArrowUp"}, socket) do - return_if_dropdown_closed(socket, fn -> - current = socket.assigns.focused_member_index - - new_index = - case current do - nil -> 0 - 0 -> 0 - index -> index - 1 - end - - {:noreply, assign(socket, focused_member_index: new_index)} - end) - end - - @impl true - def handle_event("member_dropdown_keydown", %{"key" => "Enter"}, socket) do - return_if_dropdown_closed(socket, fn -> - select_focused_member(socket) - end) - end - - @impl true - def handle_event("member_dropdown_keydown", %{"key" => "Escape"}, socket) do - return_if_dropdown_closed(socket, fn -> - {:noreply, assign(socket, show_member_dropdown: false, focused_member_index: nil)} - end) - end - - @impl true - def handle_event("member_dropdown_keydown", _params, socket) do - # Ignore other keys - {:noreply, socket} + def handle_event("member_dropdown_keydown", params, socket) do + MemberDropdownNav.handle_keydown(params, socket, fn -> select_focused_member(socket) end) end @impl true @@ -778,17 +731,6 @@ defmodule MvWeb.UserLive.Form do @spec notify_parent(any()) :: {module(), any()} defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) - # Helper to ignore keyboard events when dropdown is closed - @spec return_if_dropdown_closed(Phoenix.LiveView.Socket.t(), function()) :: - {:noreply, Phoenix.LiveView.Socket.t()} - defp return_if_dropdown_closed(socket, func) do - if socket.assigns.show_member_dropdown do - func.() - else - {:noreply, socket} - end - end - # Select the currently focused member from the dropdown @spec select_focused_member(Phoenix.LiveView.Socket.t()) :: {:noreply, Phoenix.LiveView.Socket.t()}