Enhances accessibiity closes #421 #450

Merged
carla merged 15 commits from feat/421_accessibility into main 2026-02-26 21:03:02 +01:00
8 changed files with 85 additions and 41 deletions
Showing only changes of commit 9751525a0c - Show all commits

View file

@ -19,8 +19,8 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
assigns = assign(assigns, :field_type_label, &MvWeb.Translations.FieldTypes.label/1) assigns = assign(assigns, :field_type_label, &MvWeb.Translations.FieldTypes.label/1)
~H""" ~H"""
<div id={@id} class="mt-8"> <div id={@id}>
<div class="flex"> <div :if={!@show_form} class="flex">
<p class="text-sm text-base-content/70"> <p class="text-sm text-base-content/70">
{gettext("These will appear in addition to other data when adding new members.")} {gettext("These will appear in addition to other data when adding new members.")}
</p> </p>
@ -118,15 +118,15 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
<div> <div>
<p class="font-semibold"> <p class="font-semibold">
{ngettext( {ngettext(
"%{count} member has a value assigned for this custom field.", "%{count} member has a value assigned for this datafield.",
"%{count} members have values assigned for this custom field.", "%{count} members have values assigned for this datafield.",
@custom_field_to_delete.assigned_members_count, @custom_field_to_delete.assigned_members_count,
count: @custom_field_to_delete.assigned_members_count count: @custom_field_to_delete.assigned_members_count
)} )}
</p> </p>
<p class="mt-2 text-sm"> <p class="mt-2 text-sm">
{gettext( {gettext(
"All custom field values will be permanently deleted when you delete this custom field." "All datafield values will be permanently deleted when you delete this datfield."
)} )}
</p> </p>
</div> </div>
@ -192,8 +192,8 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
@impl true @impl true
def update(assigns, socket) do def update(assigns, socket) do
# Track previous show_form state to detect when form is closed # Use socket state so send_update(open_delete_for_id: ...) does not trigger false "form closed"
previous_show_form = Map.get(socket.assigns, :show_form, false) previous_show_form = socket.assigns[:show_form] || false
# If show_form is explicitly provided in assigns, reset editing state # If show_form is explicitly provided in assigns, reset editing state
socket = socket =
@ -205,13 +205,6 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
socket socket
end end
# Detect when form is closed (show_form changes from true to false)
new_show_form = Map.get(assigns, :show_form, false)
if previous_show_form and not new_show_form do
send(self(), {:editing_section_changed, nil})
end
# Get actor from assigns or fall back to socket assigns # Get actor from assigns or fall back to socket assigns
actor = Map.get(assigns, :actor, socket.assigns[:actor]) actor = Map.get(assigns, :actor, socket.assigns[:actor])
@ -246,6 +239,13 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
|> assign(:open_delete_for_id, nil) |> assign(:open_delete_for_id, nil)
end end
# Detect form closed only from final socket state (not from assigns alone)
current_show_form = socket.assigns[:show_form] || false
if previous_show_form and not current_show_form do
send(self(), {:editing_section_changed, nil})
end
{:ok, socket} {:ok, socket}
end end

View file

@ -29,27 +29,47 @@ defmodule MvWeb.DatafieldsLive do
<.header> <.header>
{gettext("Datafields")} {gettext("Datafields")}
<:subtitle> <:subtitle>
{gettext("Configure which data you want to save for your members. Define individual datafields.")} {gettext(
"Configure which data you want to save for your members. Define individual datafields."
)}
</:subtitle> </:subtitle>
</.header> </.header>
<.form_section title={gettext("Member fields")}> <%!-- Overview: both sections with form_section wrappers --%>
<div :if={@active_editing_section == nil} class="mt-6 space-y-6">
<.form_section title={gettext("Personal Data")}>
<.live_component
module={MvWeb.MemberFieldLive.IndexComponent}
id="member-fields-component"
settings={@settings}
/>
</.form_section>
<.form_section title={gettext("Individual Datafields")}>
<.live_component
module={MvWeb.CustomFieldLive.IndexComponent}
id="custom-fields-component"
actor={@current_user}
/>
</.form_section>
</div>
<%!-- Edit mode: only the active section, no section title/card wrapper --%>
<div :if={@active_editing_section == :member_fields} class="mt-6">
<.live_component <.live_component
:if={@active_editing_section != :custom_fields}
module={MvWeb.MemberFieldLive.IndexComponent} module={MvWeb.MemberFieldLive.IndexComponent}
id="member-fields-component" id="member-fields-component"
settings={@settings} settings={@settings}
/> />
</.form_section> </div>
<.form_section title={gettext("Custom fields")}> <div :if={@active_editing_section == :custom_fields} class="mt-6">
<.live_component <.live_component
:if={@active_editing_section != :member_fields}
module={MvWeb.CustomFieldLive.IndexComponent} module={MvWeb.CustomFieldLive.IndexComponent}
id="custom-fields-component" id="custom-fields-component"
actor={@current_user} actor={@current_user}
/> />
</.form_section> </div>
</Layouts.app> </Layouts.app>
""" """
end end

View file

@ -25,7 +25,7 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do
~H""" ~H"""
<div id={@id}> <div id={@id}>
<p class="text-sm text-base-content/70 mb-4"> <p :if={!@show_form} class="text-sm text-base-content/70 mb-4">
{gettext( {gettext(
"These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview." "These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview."
)} )}
@ -100,8 +100,8 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do
@impl true @impl true
def update(assigns, socket) do def update(assigns, socket) do
# Track previous show_form state to detect when form is closed # Use socket state so send_update(show_form: false) is the only trigger for "form closed"
previous_show_form = Map.get(socket.assigns, :show_form, false) previous_show_form = socket.assigns[:show_form] || false
# If show_form is explicitly provided in assigns, reset editing state # If show_form is explicitly provided in assigns, reset editing state
socket = socket =
@ -113,20 +113,22 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do
socket socket
end end
# Detect when form is closed (show_form changes from true to false) socket =
new_show_form = Map.get(assigns, :show_form, false) socket
|> assign(assigns)
|> assign_new(:settings, fn -> get_settings() end)
|> assign_new(:show_form, fn -> false end)
|> assign_new(:form_id, fn -> "member-field-form-new" end)
|> assign_new(:editing_member_field, fn -> nil end)
if previous_show_form and not new_show_form do # Detect form closed only from final socket state (not from assigns alone)
current_show_form = socket.assigns[:show_form] || false
if previous_show_form and not current_show_form do
send(self(), {:editing_section_changed, nil}) send(self(), {:editing_section_changed, nil})
end end
{:ok, {:ok, socket}
socket
|> assign(assigns)
|> assign_new(:settings, fn -> get_settings() end)
|> assign_new(:show_form, fn -> false end)
|> assign_new(:form_id, fn -> "member-field-form-new" end)
|> assign_new(:editing_member_field, fn -> nil end)}
end end
@impl true @impl true

View file

@ -71,6 +71,10 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
# Modal should be visible # Modal should be visible
assert has_element?(view, "#delete-custom-field-modal") assert has_element?(view, "#delete-custom-field-modal")
# Edit mode: section titles must not reappear when modal opens (regression)
refute has_element?(view, "h2", "Member fields")
refute has_element?(view, "h2", "Custom fields")
# Should show correct member count (1 member) # Should show correct member count (1 member)
assert render(view) =~ "1 member has a value assigned for this custom field" assert render(view) =~ "1 member has a value assigned for this custom field"

View file

@ -83,6 +83,21 @@ defmodule MvWeb.MemberFieldLive.IndexComponentTest do
end end
end end
describe "edit mode visibility" do
test "clicking member field row shows only form, no section titles", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/datafields")
# Row click is on the first td (no col_click); click that cell to open edit form
view
|> element("tr#member_field-first_name td:first-child")
|> render_click()
assert has_element?(view, "#member-field-form-first_name")
refute has_element?(view, "h2", "Custom fields")
refute has_element?(view, "h2", "Member fields")
end
end
describe "required fields" do describe "required fields" do
setup do setup do
{:ok, settings} = Membership.get_settings() {:ok, settings} = Membership.get_settings()

View file

@ -29,9 +29,8 @@ defmodule MvWeb.StatisticsLiveTest do
test "page shows overview of all relevant years without year selector", %{conn: conn} do test "page shows overview of all relevant years without year selector", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/statistics") {:ok, _view, html} = live(conn, ~p"/statistics")
# No year dropdown: single select for year should not be present as main control # Page shows multi-year data (member numbers by year) and year column; no single-year selector as main control
assert html =~ "Overview" or html =~ "overview" assert html =~ "Member numbers by year"
# table header or legend
assert html =~ "Year" assert html =~ "Year"
end end

View file

@ -123,13 +123,17 @@ defmodule MvWeb.UserLive.IndexTest do
{:ok, index_view, _html} = live(conn, "/users") {:ok, index_view, _html} = live(conn, "/users")
assert render(index_view) =~ "delete-me@example.com" assert render(index_view) =~ "delete-me@example.com"
# Navigate to user show and trigger delete from Danger zone # Navigate to user show, open delete modal, then confirm in modal (WCAG modal pattern)
{:ok, show_view, _html} = live(conn, "/users/#{user.id}") {:ok, show_view, _html} = live(conn, "/users/#{user.id}")
show_view show_view
|> element("[data-testid=user-delete]") |> element("[data-testid=user-delete]")
|> render_click() |> render_click()
show_view
|> element("#delete-user-modal button", "Delete")
|> render_click()
# Should redirect to index # Should redirect to index
assert_redirect(show_view, "/users") assert_redirect(show_view, "/users")