fix: address review comments
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/promote/production Build is passing

This commit is contained in:
Simon 2026-02-17 15:30:23 +01:00
parent b1a9eb8b1d
commit 911f308a67
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
7 changed files with 88 additions and 164 deletions

View file

@ -356,9 +356,9 @@ lib/
- Screen readers must be able to navigate and understand the interface - Screen readers must be able to navigate and understand the interface
- ARIA labels and roles must be properly set - ARIA labels and roles must be properly set
**Group Badges in Member Overview:** **Group Badges and Links in Member Overview / Detail:**
- Badges must have `role="status"` and appropriate `aria-label` attributes - Use `aria-label` to indicate group membership (e.g. "Member of group X"). Do not use `role="status"` on badges or links—that role is for live regions (screen reader announcements), not for navigation or static labels.
- Badge title should indicate group membership - Badge/link text or title should indicate group membership for screen readers.
**Clickable Group Badge (for filtering) - Optional:** **Clickable Group Badge (for filtering) - Optional:**

View file

@ -322,7 +322,6 @@
<%= for group <- (member.groups || []) do %> <%= for group <- (member.groups || []) do %>
<span <span
class="badge badge-outline badge-primary" class="badge badge-outline badge-primary"
role="status"
aria-label={gettext("Member of group %{name}", name: group.name)} aria-label={gettext("Member of group %{name}", name: group.name)}
> >
{group.name} {group.name}

View file

@ -12,7 +12,7 @@ defmodule MvWeb.MemberLive.Show do
## Sections ## Sections
- Personal Data: Name, address, contact information, membership dates, notes - Personal Data: Name, address, contact information, membership dates, notes
- Custom Fields: Dynamic fields in uniform grid layout (sorted by name) - Custom Fields: Dynamic fields in uniform grid layout (sorted by name)
- Groups: Group links (buttons) in Personal Data section, below Linked User - Groups: Links to group detail pages in Personal Data section
- Payment Data: Membership fee type and cycle status - Payment Data: Membership fee type and cycle status
- Membership Fees: Tab showing all membership fee cycles with status management (via MembershipFeesComponent) - Membership Fees: Tab showing all membership fee cycles with status management (via MembershipFeesComponent)
@ -148,24 +148,24 @@ defmodule MvWeb.MemberLive.Show do
</div> </div>
<% end %> <% end %>
<%!-- Groups (in Personal Data, below Linked User) --%> <%!-- Groups (in Personal Data) --%>
<% groups = @member.groups || [] %>
<div> <div>
<.data_field label={gettext("Groups")}> <.data_field label={gettext("Groups")}>
<%= if Enum.any?(@member.groups || []) do %> <%= if Enum.empty?(groups) do %>
<span class="text-base-content/70 italic">{gettext("No groups")}</span>
<% else %>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<%= for group <- (@member.groups || []) do %> <%= for group <- groups do %>
<.link <.link
navigate={~p"/groups/#{group.slug}"} navigate={~p"/groups/#{group.slug}"}
class="btn btn-xs btn-outline btn-primary" class="btn btn-xs btn-outline btn-primary"
role="status"
aria-label={gettext("Member of group %{name}", name: group.name)} aria-label={gettext("Member of group %{name}", name: group.name)}
> >
{group.name} {group.name}
</.link> </.link>
<% end %> <% end %>
</div> </div>
<% else %>
<span class="text-base-content/70 italic">{gettext("No groups")}</span>
<% end %> <% end %>
</.data_field> </.data_field>
</div> </div>

View file

@ -2616,12 +2616,3 @@ msgstr "Anzahl Mitglieder:"
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "PDF" msgid "PDF"
msgstr "PDF" msgstr "PDF"
#~ #: lib/mv_web/live/global_settings_live.ex
#~ #, elixir-autogen, elixir-format, fuzzy
#~ msgid "Custom Fields in CSV Import"
#~ msgstr "Benutzerdefinierte Felder"
#~ #, elixir-autogen, elixir-format
#~ msgid "Failed to prepare CSV import: %{error}"
#~ msgstr "Das Vorbereiten des CSV Imports ist gescheitert: %{error}"

View file

@ -2617,8 +2617,3 @@ msgstr "Member count:"
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "PDF" msgid "PDF"
msgstr "" msgstr ""
#~ #: lib/mv_web/live/global_settings_live.ex
#~ #, elixir-autogen, elixir-format, fuzzy
#~ msgid "Custom Fields in CSV Import"
#~ msgstr ""

View file

@ -3,7 +3,7 @@ defmodule MvWeb.MemberLive.IndexGroupsAccessibilityTest do
Tests for accessibility of groups feature in the member overview. Tests for accessibility of groups feature in the member overview.
Tests cover: Tests cover:
- Badges have role="status" and aria-label - Badges have aria-label for group membership (no role="status"; reserved for live regions)
- Filter dropdown has aria-label - Filter dropdown has aria-label
- Sort header has aria-label for screen reader - Sort header has aria-label for screen reader
- Keyboard navigation works (Tab through filter, sort header) - Keyboard navigation works (Tab through filter, sort header)
@ -44,7 +44,7 @@ defmodule MvWeb.MemberLive.IndexGroupsAccessibilityTest do
end end
@tag :ui @tag :ui
test "group badges have role and aria-label", %{ test "group badges have aria-label for screen readers", %{
conn: conn, conn: conn,
member1: member1, member1: member1,
group1: group1 group1: group1
@ -52,8 +52,8 @@ defmodule MvWeb.MemberLive.IndexGroupsAccessibilityTest do
conn = conn_with_oidc_user(conn) conn = conn_with_oidc_user(conn)
{:ok, view, html} = live(conn, "/members") {:ok, view, html} = live(conn, "/members")
# Verify badges have role="status" and aria-label containing the group name # Verify badges have aria-label containing the group name (no role=status on badges)
assert has_element?(view, "span[role='status'][aria-label*='#{group1.name}']") assert has_element?(view, "span[aria-label*='#{group1.name}']")
assert html =~ group1.name assert html =~ group1.name
# Verify member1's row contains the badge # Verify member1's row contains the badge

View file

@ -4,10 +4,10 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do
Tests cover: Tests cover:
- Groups in Personal Data (with and without groups) - Groups in Personal Data (with and without groups)
- Group buttons with correct names and links to group detail pages - Group links with correct names and links to group detail pages
- Edge cases (one group, many groups) - Edge cases (one group, many groups)
- Security: groups visible only when user may view member - Security: groups visible only when user may view member
- Accessibility: group links have role and aria-label - Accessibility: group links have aria-label for screen readers
## Note on async ## Note on async
async: false to avoid PostgreSQL deadlocks when creating members and groups async: false to avoid PostgreSQL deadlocks when creating members and groups
@ -22,15 +22,16 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do
describe "groups section" do describe "groups section" do
setup do setup do
system_actor = Mv.Helpers.SystemActor.get_system_actor() actor = Mv.Helpers.SystemActor.get_system_actor()
{:ok, member} = {:ok, member} =
Mv.Membership.create_member( create_member(actor, %{
%{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"}, first_name: "Alice",
actor: system_actor last_name: "Anderson",
) email: "alice@example.com"
})
%{member: member, actor: system_actor} %{member: member, actor: actor}
end end
test "displays Groups section when member has at least one group", %{ test "displays Groups section when member has at least one group", %{
@ -38,15 +39,8 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do
member: member, member: member,
actor: actor actor: actor
} do } do
{:ok, group} = {:ok, group} = create_group(actor, "Board Members")
Group {:ok, _mg} = add_member_to_group(member, group, actor)
|> Ash.Changeset.for_create(:create, %{name: "Board Members"})
|> Ash.create(actor: actor)
{:ok, _mg} =
MemberGroup
|> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id})
|> Ash.create(actor: actor)
conn = conn_with_oidc_user(conn) conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, ~p"/members/#{member}") {:ok, _view, html} = live(conn, ~p"/members/#{member}")
@ -55,30 +49,15 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do
assert html =~ group.name assert html =~ group.name
end end
test "displays all groups as badges with correct names when member is in multiple groups", %{ test "displays all group links with correct names when member is in multiple groups", %{
conn: conn, conn: conn,
member: member, member: member,
actor: actor actor: actor
} do } do
{:ok, group1} = {:ok, group1} = create_group(actor, "Board Members")
Group {:ok, group2} = create_group(actor, "Active Members")
|> Ash.Changeset.for_create(:create, %{name: "Board Members"}) {:ok, _mg1} = add_member_to_group(member, group1, actor)
|> Ash.create(actor: actor) {:ok, _mg2} = add_member_to_group(member, group2, actor)
{:ok, group2} =
Group
|> Ash.Changeset.for_create(:create, %{name: "Active Members"})
|> Ash.create(actor: actor)
{:ok, _mg1} =
MemberGroup
|> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group1.id})
|> Ash.create(actor: actor)
{:ok, _mg2} =
MemberGroup
|> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group2.id})
|> Ash.create(actor: actor)
conn = conn_with_oidc_user(conn) conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, ~p"/members/#{member}") {:ok, _view, html} = live(conn, ~p"/members/#{member}")
@ -100,30 +79,15 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do
assert html =~ gettext("No groups") assert html =~ gettext("No groups")
end end
test "groups are loaded with member (single request returns all group names)", %{ test "renders all group names when member has multiple groups", %{
conn: conn, conn: conn,
member: member, member: member,
actor: actor actor: actor
} do } do
{:ok, group1} = {:ok, group1} = create_group(actor, "Alpha")
Group {:ok, group2} = create_group(actor, "Beta")
|> Ash.Changeset.for_create(:create, %{name: "Alpha"}) {:ok, _mg1} = add_member_to_group(member, group1, actor)
|> Ash.create(actor: actor) {:ok, _mg2} = add_member_to_group(member, group2, actor)
{:ok, group2} =
Group
|> Ash.Changeset.for_create(:create, %{name: "Beta"})
|> Ash.create(actor: actor)
{:ok, _mg1} =
MemberGroup
|> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group1.id})
|> Ash.create(actor: actor)
{:ok, _mg2} =
MemberGroup
|> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group2.id})
|> Ash.create(actor: actor)
conn = conn_with_oidc_user(conn) conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, ~p"/members/#{member}") {:ok, _view, html} = live(conn, ~p"/members/#{member}")
@ -135,41 +99,29 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do
describe "groups section links" do describe "groups section links" do
setup do setup do
system_actor = Mv.Helpers.SystemActor.get_system_actor() actor = Mv.Helpers.SystemActor.get_system_actor()
{:ok, member} = {:ok, member} =
Mv.Membership.create_member( create_member(actor, %{first_name: "Bob", last_name: "Brown", email: "bob@example.com"})
%{first_name: "Bob", last_name: "Brown", email: "bob@example.com"},
actor: system_actor
)
{:ok, group} =
Group
|> Ash.Changeset.for_create(:create, %{name: "Board Members"})
|> Ash.create(actor: system_actor)
{:ok, _mg} =
MemberGroup
|> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id})
|> Ash.create(actor: system_actor)
{:ok, group} = create_group(actor, "Board Members")
{:ok, _mg} = add_member_to_group(member, group, actor)
%{member: member, group: group} %{member: member, group: group}
end end
test "each group badge links to group detail page with correct slug", %{ test "each group link goes to group detail page with correct slug", %{
conn: conn, conn: conn,
member: member, member: member,
group: group group: group
} do } do
conn = conn_with_oidc_user(conn) conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, ~p"/members/#{member}") {:ok, view, _html} = live(conn, ~p"/members/#{member}")
# Link to group detail: /groups/:slug (slug is URL-friendly, e.g. "board-members") # Link to group detail: /groups/:slug (slug is URL-friendly, e.g. "board-members")
assert html =~ assert has_element?(view, "a[href*='/groups/#{group.slug}']", group.name)
~r/href="[^"]*\/groups\/#{Regex.escape(group.slug)}"|navigate="[^"]*\/groups\/#{Regex.escape(group.slug)}"/
end end
test "clicking group badge navigates to group detail page", %{ test "clicking group link navigates to group detail page", %{
conn: conn, conn: conn,
member: member, member: member,
group: group group: group
@ -187,31 +139,25 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do
describe "groups section edge cases" do describe "groups section edge cases" do
setup do setup do
system_actor = Mv.Helpers.SystemActor.get_system_actor() actor = Mv.Helpers.SystemActor.get_system_actor()
{:ok, member} = {:ok, member} =
Mv.Membership.create_member( create_member(actor, %{
%{first_name: "Charlie", last_name: "Clark", email: "charlie@example.com"}, first_name: "Charlie",
actor: system_actor last_name: "Clark",
) email: "charlie@example.com"
})
%{member: member, actor: system_actor} %{member: member, actor: actor}
end end
test "member in exactly one group shows single badge", %{ test "member in exactly one group shows single link", %{
conn: conn, conn: conn,
member: member, member: member,
actor: actor actor: actor
} do } do
{:ok, group} = {:ok, group} = create_group(actor, "Solo Group")
Group {:ok, _mg} = add_member_to_group(member, group, actor)
|> Ash.Changeset.for_create(:create, %{name: "Solo Group"})
|> Ash.create(actor: actor)
{:ok, _mg} =
MemberGroup
|> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id})
|> Ash.create(actor: actor)
conn = conn_with_oidc_user(conn) conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, ~p"/members/#{member}") {:ok, _view, html} = live(conn, ~p"/members/#{member}")
@ -220,7 +166,7 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do
assert html =~ group.name assert html =~ group.name
end end
test "member in many groups shows all badges", %{ test "member in many groups shows all group links", %{
conn: conn, conn: conn,
member: member, member: member,
actor: actor actor: actor
@ -229,19 +175,12 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do
groups = groups =
Enum.map(group_names, fn name -> Enum.map(group_names, fn name ->
{:ok, g} = {:ok, g} = create_group(actor, name)
Group
|> Ash.Changeset.for_create(:create, %{name: name})
|> Ash.create(actor: actor)
g g
end) end)
for g <- groups do for g <- groups do
{:ok, _mg} = {:ok, _mg} = add_member_to_group(member, g, actor)
MemberGroup
|> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: g.id})
|> Ash.create(actor: actor)
end end
conn = conn_with_oidc_user(conn) conn = conn_with_oidc_user(conn)
@ -258,23 +197,17 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do
describe "groups section with read_only user" do describe "groups section with read_only user" do
@tag role: :read_only @tag role: :read_only
test "user with read permission sees Groups section", %{conn: conn} do test "user with read permission sees Groups section", %{conn: conn} do
system_actor = Mv.Helpers.SystemActor.get_system_actor() actor = Mv.Helpers.SystemActor.get_system_actor()
{:ok, member} = {:ok, member} =
Mv.Membership.create_member( create_member(actor, %{
%{first_name: "Diana", last_name: "Davis", email: "diana@example.com"}, first_name: "Diana",
actor: system_actor last_name: "Davis",
) email: "diana@example.com"
})
{:ok, group} = {:ok, group} = create_group(actor, "Readers")
Group {:ok, _mg} = add_member_to_group(member, group, actor)
|> Ash.Changeset.for_create(:create, %{name: "Readers"})
|> Ash.create(actor: system_actor)
{:ok, _mg} =
MemberGroup
|> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id})
|> Ash.create(actor: system_actor)
{:ok, _view, html} = live(conn, ~p"/members/#{member}") {:ok, _view, html} = live(conn, ~p"/members/#{member}")
@ -285,28 +218,17 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do
describe "groups section accessibility" do describe "groups section accessibility" do
setup do setup do
system_actor = Mv.Helpers.SystemActor.get_system_actor() actor = Mv.Helpers.SystemActor.get_system_actor()
{:ok, member} = {:ok, member} =
Mv.Membership.create_member( create_member(actor, %{first_name: "Eve", last_name: "Evans", email: "eve@example.com"})
%{first_name: "Eve", last_name: "Evans", email: "eve@example.com"},
actor: system_actor
)
{:ok, group} =
Group
|> Ash.Changeset.for_create(:create, %{name: "A11y Group"})
|> Ash.create(actor: system_actor)
{:ok, _mg} =
MemberGroup
|> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id})
|> Ash.create(actor: system_actor)
{:ok, group} = create_group(actor, "A11y Group")
{:ok, _mg} = add_member_to_group(member, group, actor)
%{member: member, group: group} %{member: member, group: group}
end end
test "group badges have role and aria-label for screen readers", %{ test "group links have aria-label for screen readers", %{
conn: conn, conn: conn,
member: member, member: member,
group: group group: group
@ -316,8 +238,25 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do
assert html =~ group.name assert html =~ group.name
# Badge has role="status" and aria-label indicating group membership (architecture: "Member of group X") # Group link has aria-label indicating group membership for screen readers
assert has_element?(view, "[role='status'][aria-label*='#{group.name}']") assert has_element?(view, "a[aria-label*='#{group.name}']")
end end
end end
# Helpers to reduce setup duplication (create member/group, assign member to group).
defp create_member(actor, attrs) do
Mv.Membership.create_member(attrs, actor: actor)
end
defp create_group(actor, name) do
Group
|> Ash.Changeset.for_create(:create, %{name: name})
|> Ash.create(actor: actor)
end
defp add_member_to_group(member, group, actor) do
MemberGroup
|> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id})
|> Ash.create(actor: actor)
end
end end