feat: add accessible drag&drop table component
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Simon 2026-03-10 15:40:28 +01:00
parent fa738aae88
commit 05e2a298fe
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
9 changed files with 386 additions and 15 deletions

View file

@ -160,7 +160,10 @@ defmodule MvWeb.GlobalSettingsLive do
type="button"
variant="primary"
phx-click="toggle_add_field_dropdown"
disabled={Enum.empty?(@available_join_form_member_fields) and Enum.empty?(@available_join_form_custom_fields)}
disabled={
Enum.empty?(@available_join_form_member_fields) and
Enum.empty?(@available_join_form_custom_fields)
}
aria-haspopup="listbox"
aria-expanded={to_string(@show_add_field_dropdown)}
>
@ -190,7 +193,15 @@ defmodule MvWeb.GlobalSettingsLive do
{field.label}
</div>
</div>
<div :if={not Enum.empty?(@available_join_form_custom_fields)} class={if(Enum.empty?(@available_join_form_member_fields), do: "pt-2", else: "border-t border-base-300")}>
<div
:if={not Enum.empty?(@available_join_form_custom_fields)}
class={
if(Enum.empty?(@available_join_form_member_fields),
do: "pt-2",
else: "border-t border-base-300"
)
}
>
<div class="px-4 py-1 text-xs font-semibold text-base-content/60 uppercase tracking-wide">
{gettext("Individual fields")}
</div>
@ -213,12 +224,13 @@ defmodule MvWeb.GlobalSettingsLive do
{gettext("No fields selected. Add at least the email field.")}
</p>
<%!-- Fields table (compact width) --%>
<%!-- Fields table (compact width, reorderable) --%>
<div :if={not Enum.empty?(@join_form_fields)} class="mb-4 max-w-2xl">
<.table
<.sortable_table
id="join-form-fields-table"
rows={@join_form_fields}
row_id={fn field -> "join-field-#{field.id}" end}
reorder_event="reorder_join_form_field"
>
<:col :let={field} label={gettext("Field")} class="min-w-[14rem]">
{field.label}
@ -250,7 +262,10 @@ defmodule MvWeb.GlobalSettingsLive do
</.button>
</.tooltip>
</:action>
</.table>
</.sortable_table>
<p class="mt-2 text-sm text-base-content/60">
{gettext("The order of rows determines the field order in the join form.")}
</p>
</div>
</div>
</.form_section>
@ -642,6 +657,7 @@ defmodule MvWeb.GlobalSettingsLive do
custom_fields = socket.assigns.join_form_custom_fields
new_fields = Enum.reject(current, &(&1.id == field_id))
new_ids = Enum.map(new_fields, & &1.id)
%{member_fields: new_member, custom_fields: new_custom} =
build_available_join_form_fields(new_ids, custom_fields)
@ -670,6 +686,27 @@ defmodule MvWeb.GlobalSettingsLive do
{:noreply, socket}
end
@impl true
def handle_event(
"reorder_join_form_field",
%{"from_index" => from_idx, "to_index" => to_idx},
socket
)
when is_integer(from_idx) and is_integer(to_idx) do
fields = socket.assigns.join_form_fields
new_fields = reorder_list(fields, from_idx, to_idx)
socket =
socket
|> assign(:join_form_fields, new_fields)
|> persist_join_form_settings()
{:noreply, socket}
end
# Ignore malformed reorder events (e.g. nil indices from aborted drags)
def handle_event("reorder_join_form_field", _params, socket), do: {:noreply, socket}
defp persist_join_form_settings(socket) do
settings = socket.assigns.settings
field_ids = Enum.map(socket.assigns.join_form_fields, & &1.id)
@ -990,6 +1027,7 @@ defmodule MvWeb.GlobalSettingsLive do
required_config = settings.join_form_field_required || %{}
join_form_fields = build_join_form_fields(field_ids, required_config, custom_fields)
%{member_fields: member_avail, custom_fields: custom_avail} =
build_available_join_form_fields(field_ids, custom_fields)
@ -1054,6 +1092,12 @@ defmodule MvWeb.GlobalSettingsLive do
defp toggle_required_if_matches(field, _field_id), do: field
defp reorder_list(list, from_index, to_index) do
item = Enum.at(list, from_index)
rest = List.delete_at(list, from_index)
List.insert_at(rest, to_index, item)
end
defp member_field_id_strings do
Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)
end