refactor(web): share member-dropdown keyboard navigation between LiveViews
This commit is contained in:
parent
164826d3aa
commit
561779e704
3 changed files with 89 additions and 119 deletions
|
|
@ -23,6 +23,7 @@ defmodule MvWeb.GroupLive.Show do
|
||||||
|
|
||||||
alias Mv.Membership
|
alias Mv.Membership
|
||||||
alias MvWeb.Helpers.MemberHelpers, as: MemberHelpers
|
alias MvWeb.Helpers.MemberHelpers, as: MemberHelpers
|
||||||
|
alias MvWeb.Live.MemberDropdownNav
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
|
|
@ -566,56 +567,8 @@ defmodule MvWeb.GroupLive.Show do
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("member_dropdown_keydown", %{"key" => "ArrowDown"}, socket) do
|
def handle_event("member_dropdown_keydown", params, socket) do
|
||||||
return_if_dropdown_closed(socket, fn ->
|
MemberDropdownNav.handle_keydown(params, socket, fn -> select_focused_member(socket) end)
|
||||||
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}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
@ -705,14 +658,6 @@ defmodule MvWeb.GroupLive.Show do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper functions
|
# 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
|
defp select_focused_member(socket) do
|
||||||
case socket.assigns.focused_member_index do
|
case socket.assigns.focused_member_index do
|
||||||
nil ->
|
nil ->
|
||||||
|
|
|
||||||
83
lib/mv_web/live/member_dropdown_nav.ex
Normal file
83
lib/mv_web/live/member_dropdown_nav.ex
Normal file
|
|
@ -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
|
||||||
|
|
@ -43,6 +43,7 @@ defmodule MvWeb.UserLive.Form do
|
||||||
alias Mv.Membership
|
alias Mv.Membership
|
||||||
alias Mv.Membership.Member, as: MemberResource
|
alias Mv.Membership.Member, as: MemberResource
|
||||||
alias MvWeb.Helpers.MemberHelpers
|
alias MvWeb.Helpers.MemberHelpers
|
||||||
|
alias MvWeb.Live.MemberDropdownNav
|
||||||
|
|
||||||
import MvWeb.LiveHelpers, only: [current_actor: 1, submit_form: 3]
|
import MvWeb.LiveHelpers, only: [current_actor: 1, submit_form: 3]
|
||||||
import MvWeb.Authorization, only: [can?: 3]
|
import MvWeb.Authorization, only: [can?: 3]
|
||||||
|
|
@ -571,56 +572,8 @@ defmodule MvWeb.UserLive.Form do
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("member_dropdown_keydown", %{"key" => "ArrowDown"}, socket) do
|
def handle_event("member_dropdown_keydown", params, socket) do
|
||||||
return_if_dropdown_closed(socket, fn ->
|
MemberDropdownNav.handle_keydown(params, socket, fn -> select_focused_member(socket) end)
|
||||||
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}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
@ -778,17 +731,6 @@ defmodule MvWeb.UserLive.Form do
|
||||||
@spec notify_parent(any()) :: {module(), any()}
|
@spec notify_parent(any()) :: {module(), any()}
|
||||||
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
|
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
|
# Select the currently focused member from the dropdown
|
||||||
@spec select_focused_member(Phoenix.LiveView.Socket.t()) ::
|
@spec select_focused_member(Phoenix.LiveView.Socket.t()) ::
|
||||||
{:noreply, Phoenix.LiveView.Socket.t()}
|
{:noreply, Phoenix.LiveView.Socket.t()}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue