From 4ac56958b4afcbe4bc6a16f8c45e26a5bdde49b0 Mon Sep 17 00:00:00 2001 From: carla Date: Thu, 26 Feb 2026 13:37:35 +0100 Subject: [PATCH] feat: keep empty cells consistent empty --- CODE_GUIDELINES.md | 4 + DESIGN_DUIDELINES.md | 6 + lib/mv_web/components/core_components.ex | 75 ++++++++ .../live/custom_field_live/index_component.ex | 2 +- lib/mv_web/live/group_live/index.ex | 6 +- lib/mv_web/live/group_live/show.ex | 6 +- lib/mv_web/live/member_live/index.html.heex | 31 ++-- lib/mv_web/live/member_live/show.ex | 31 ++-- .../show/membership_fees_component.ex | 22 ++- .../live/membership_fee_settings_live.ex | 2 +- lib/mv_web/live/role_live/index.html.heex | 2 +- lib/mv_web/live/user_live/index.html.heex | 24 +-- priv/gettext/de/LC_MESSAGES/default.po | 171 +++--------------- priv/gettext/default.pot | 55 +++--- priv/gettext/en/LC_MESSAGES/default.po | 166 +++-------------- .../live/custom_field_live/deletion_test.exs | 6 +- .../member_live/index_groups_display_test.exs | 14 ++ test/mv_web/user_live/index_test.exs | 12 +- 18 files changed, 263 insertions(+), 372 deletions(-) diff --git a/CODE_GUIDELINES.md b/CODE_GUIDELINES.md index bbc5ee4..48e2e8e 100644 --- a/CODE_GUIDELINES.md +++ b/CODE_GUIDELINES.md @@ -2779,6 +2779,10 @@ Building accessible applications ensures that all users, including those with di - When `row_click` is set, the first column that does not use `col_click` gets `tabindex="0"` and `role="button"` so each row is reachable via Tab. The `TableRowKeydown` hook triggers the row action on Enter and Space (WCAG 2.1.1). Use `row_id` and `row_tooltip` for all clickable tables (e.g. Groups, Users, Roles, Members, Custom Fields, Member Fields) so the table is fully keyboard accessible. +**Empty table cells (missing values):** + +- Do not use dashes ("-", "—", "–") or "n/a" as placeholders. Use CoreComponents `<.empty_cell sr_text="…">` for a cell with no value, or `<.maybe_value value={…} empty_sr_text="…">` when content is conditional. The cell is visually empty; screen readers get the `sr_text` (e.g. "No cycle", "No group assignment", "Not specified"). See Design Guidelines §8.6. + **Tab Order:** - Ensure logical tab order matches visual order diff --git a/DESIGN_DUIDELINES.md b/DESIGN_DUIDELINES.md index b497254..92f7a90 100644 --- a/DESIGN_DUIDELINES.md +++ b/DESIGN_DUIDELINES.md @@ -293,6 +293,12 @@ Notes: - On **mobile**, sticky headers are not used; the layout uses normal flow (header and table scroll with the page) to preserve vertical space. - When the table is inside such a scroll container, use the CoreComponents table’s `sticky_header={true}` so the table’s `` stays sticky within the scroll area on desktop (`lg:sticky lg:top-0`, opaque background `bg-base-100`, z-index). Sticky areas must not overlap content at 200% zoom; focus order must remain header → filters → table. +### 8.6 Empty table cells (missing values) +- **MUST:** Missing values in tables are shown as **visually empty cells** (no dash, no "n/a"). +- **MUST NOT:** Use dashes ("-", "—", "–") or "n/a" as placeholders for empty cells. +- **MUST:** For accessibility, render a screen-reader-only label so the cell is not announced as "blank". Use the CoreComponents `<.empty_cell sr_text="…">` for a cell that has no value, or `<.maybe_value value={…} empty_sr_text="…">` when the cell content is conditional (value present vs. absent). +- **SHOULD:** Use context-specific `sr_text` where it helps (e.g. "No cycle", "No group assignment", "Not specified"). Default for "no value" is "Not specified". + --- ## 9) Flash / Toast messages (mandatory UX) diff --git a/lib/mv_web/components/core_components.ex b/lib/mv_web/components/core_components.ex index 963c868..5f12f0a 100644 --- a/lib/mv_web/components/core_components.ex +++ b/lib/mv_web/components/core_components.ex @@ -300,6 +300,81 @@ defmodule MvWeb.CoreComponents do defp badge_style_class("outline"), do: "badge-outline" defp badge_style_class(_), do: nil + @doc """ + Renders a visually empty table cell with screen-reader-only text (WCAG). + + Use when a table cell has no value so that: + - The cell appears empty (no dash, no "n/a"). + - Screen readers still get a meaningful label (e.g. "No cycle", "No group assignment"). + + See CODE_GUIDELINES §8 (Empty table cells) and Design Guidelines §8.6. + + ## Examples + + <.empty_cell sr_text={gettext("No cycle")} /> + <.empty_cell sr_text={gettext("No group assignment")} /> + <.empty_cell sr_text={gettext("Not specified")} /> + """ + attr :sr_text, :string, + required: true, + doc: "Text read by screen readers when the cell is visually empty" + + def empty_cell(assigns) do + ~H""" + {@sr_text} + """ + end + + @doc """ + Renders content when value is present, otherwise an accessible empty cell. + + Use in table cells for optional fields: when `value` is blank, only the + screen-reader text is shown (visually empty). Otherwise the inner block is rendered. + + Blank check: `nil`, `false`, `[]`, `""`, whitespace-only string, or `%Ash.NotLoaded{}` count as empty. + + See CODE_GUIDELINES §8 (Empty table cells) and Design Guidelines §8.6. + + ## Examples + + <.maybe_value value={member.membership_fee_type} empty_sr_text={gettext("No fee type")}> + {member.membership_fee_type.name} + + <.maybe_value value={member.groups} empty_sr_text={gettext("No group assignment")}> + <%= for g <- member.groups do %> + <.badge variant="primary" style="outline">{g.name} + <% end %> + + """ + attr :value, :any, doc: "Value to check; if blank, empty_cell is rendered" + + attr :empty_sr_text, :string, + default: nil, + doc: "Screen-reader text when value is blank (default: gettext \"Not specified\")" + + slot :inner_block, required: true + + def maybe_value(assigns) do + empty_sr = assigns.empty_sr_text || gettext("Not specified") + assigns = assign(assigns, :empty_sr_text, empty_sr) + assigns = assign(assigns, :blank?, value_blank?(assigns.value)) + + ~H""" + <%= if @blank? do %> + <.empty_cell sr_text={@empty_sr_text} /> + <% else %> + {render_slot(@inner_block)} + <% end %> + """ + end + + defp value_blank?(nil), do: true + defp value_blank?(false), do: true + defp value_blank?([]), do: true + defp value_blank?(%Ash.NotLoaded{}), do: true + defp value_blank?(v) when is_binary(v), do: String.trim(v) == "" + defp value_blank?(_), do: false + @doc """ Wraps content with a DaisyUI tooltip. Use for icon-only actions, truncated content, or status badges that need explanation (Design Guidelines §8.2). diff --git a/lib/mv_web/live/custom_field_live/index_component.ex b/lib/mv_web/live/custom_field_live/index_component.ex index c4002bc..abb19df 100644 --- a/lib/mv_web/live/custom_field_live/index_component.ex +++ b/lib/mv_web/live/custom_field_live/index_component.ex @@ -126,7 +126,7 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do

{gettext( - "All datafield values will be permanently deleted when you delete this datfield." + "All datafield values will be permanently deleted when you delete this datafield." )}

diff --git a/lib/mv_web/live/group_live/index.ex b/lib/mv_web/live/group_live/index.ex index ff22b91..3aef8fb 100644 --- a/lib/mv_web/live/group_live/index.ex +++ b/lib/mv_web/live/group_live/index.ex @@ -68,11 +68,9 @@ defmodule MvWeb.GroupLive.Index do {group.name} <:col :let={group} label={gettext("Description")}> - <%= if group.description do %> + <.maybe_value value={group.description} empty_sr_text={gettext("Not specified")}> {group.description} - <% else %> - - <% end %> + <:col :let={group} label={gettext("Members")} class="text-right"> {group.member_count || 0} diff --git a/lib/mv_web/live/group_live/show.ex b/lib/mv_web/live/group_live/show.ex index bea6399..a700cd5 100644 --- a/lib/mv_web/live/group_live/show.ex +++ b/lib/mv_web/live/group_live/show.ex @@ -304,16 +304,14 @@ defmodule MvWeb.GroupLive.Show do - <%= if member.email do %> + <.maybe_value value={member.email} empty_sr_text={gettext("No email")}> {member.email} - <% else %> - - <% end %> + <%= if can?(@current_user, :update, @group) do %> diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex index c49e343..0ef541e 100644 --- a/lib/mv_web/live/member_live/index.html.heex +++ b/lib/mv_web/live/member_live/index.html.heex @@ -356,11 +356,9 @@ """ } > - <%= if member.membership_fee_type do %> + <.maybe_value value={member.membership_fee_type} empty_sr_text={gettext("Not specified")}> {member.membership_fee_type.name} - <% else %> - - <% end %> + <:col :let={member} @@ -375,7 +373,7 @@ {badge.label} <% else %> - <.badge variant="neutral">{gettext("No cycle")} + <.empty_cell sr_text={gettext("No cycle")} /> <% end %> <:col @@ -394,18 +392,17 @@ """ } > - <%= for group <- (member.groups || []) do %> - <.badge - variant="primary" - style="outline" - aria-label={gettext("Member of group %{name}", name: group.name)} - > - {group.name} - - <% end %> - <%= if (member.groups || []) == [] do %> - - <% end %> + <.maybe_value value={member.groups} empty_sr_text={gettext("No group assignment")}> + <%= for group <- (member.groups || []) do %> + <.badge + variant="primary" + style="outline" + aria-label={gettext("Member of group %{name}", name: group.name)} + > + {group.name} + + <% end %> + <:action :let={member}>
diff --git a/lib/mv_web/live/member_live/show.ex b/lib/mv_web/live/member_live/show.ex index 6645051..deb6cf0 100644 --- a/lib/mv_web/live/member_live/show.ex +++ b/lib/mv_web/live/member_live/show.ex @@ -559,7 +559,11 @@ defmodule MvWeb.MemberLive.Show do <%= if @inner_block != [] do %> {render_slot(@inner_block)} <% else %> - {display_value(@value)} + <%= if value_blank?(@value) do %> + <.empty_cell sr_text={gettext("Not set")} /> + <% else %> + {@value} + <% end %> <% end %> @@ -593,9 +597,9 @@ defmodule MvWeb.MemberLive.Show do # Helper Functions # ----------------------------------------------------------------- - defp display_value(nil), do: render_empty_value() - defp display_value(""), do: render_empty_value() - defp display_value(value), do: value + defp value_blank?(nil), do: true + defp value_blank?(v) when is_binary(v), do: String.trim(v) == "" + defp value_blank?(_), do: false defp format_status_label(:paid), do: gettext("Paid") defp format_status_label(:unpaid), do: gettext("Unpaid") @@ -684,10 +688,10 @@ defmodule MvWeb.MemberLive.Show do if String.trim(value) == "" do render_empty_value() else - assigns = %{email: value} + assigns = %{email: value, display: value} ~H""" - <.mailto_link email={@email} display={@email} /> + <.mailto_link email={@email} display={@display} /> """ end end @@ -702,17 +706,10 @@ defmodule MvWeb.MemberLive.Show do defp format_custom_field_value(value, _type), do: to_string(value) - # Renders accessible placeholder for empty values - # Uses translated text for screen readers while maintaining visual consistency - # The visual "—" is hidden from screen readers, while the translated text is only visible to screen readers + # Renders accessible empty value: visually empty, screen-reader text only (see Design Guidelines §8.6). + # Returns safe HTML so it can be used from helpers without LiveView assigns. defp render_empty_value do - assigns = %{text: gettext("Not set")} - - ~H""" - - - {@text} - - """ + text = gettext("Not set") + {:safe, ["", Phoenix.HTML.Engine.html_escape(text), ""]} end end diff --git a/lib/mv_web/live/member_live/show/membership_fees_component.ex b/lib/mv_web/live/member_live/show/membership_fees_component.ex index 1db11e3..7ed5fbe 100644 --- a/lib/mv_web/live/member_live/show/membership_fees_component.ex +++ b/lib/mv_web/live/member_live/show/membership_fees_component.ex @@ -101,7 +101,13 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do <%= for r <- receipts do %> <%= for {col_key, _header_key} <- cols do %> - {format_receipt_cell(col_key, r[col_key])} + + <%= if (cell_content = format_receipt_cell(col_key, r[col_key])) != nil do %> + {cell_content} + <% else %> + <.empty_cell sr_text={receipt_empty_sr_text(col_key)} /> + <% end %> + <% end %> <% end %> @@ -1157,7 +1163,11 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do |> Enum.map(fn {key, msgid} -> {key, Gettext.gettext(MvWeb.Gettext, msgid)} end) end - defp format_receipt_cell(:amount, nil), do: "—" + # Screen-reader text for empty receipt table cells (visually empty, A11y) + defp receipt_empty_sr_text(:status), do: gettext("Not set") + defp receipt_empty_sr_text(_), do: gettext("Not specified") + + defp format_receipt_cell(:amount, nil), do: nil defp format_receipt_cell(:amount, val) when is_number(val) do case Decimal.cast(val) do @@ -1175,7 +1185,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do defp format_receipt_cell(:amount, val), do: to_string(val) - defp format_receipt_cell(:status, nil), do: "—" + defp format_receipt_cell(:status, nil), do: nil defp format_receipt_cell(:status, val) when is_binary(val) do translate_receipt_status(val) @@ -1183,7 +1193,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do defp format_receipt_cell(:status, val), do: translate_receipt_status(to_string(val)) - defp format_receipt_cell(:receiptType, nil), do: "—" + defp format_receipt_cell(:receiptType, nil), do: nil defp format_receipt_cell(:receiptType, val) when is_binary(val) do translate_receipt_type(val) @@ -1192,7 +1202,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do defp format_receipt_cell(:receiptType, val), do: translate_receipt_type(to_string(val)) defp format_receipt_cell(col_key, nil) when col_key in [:bookingDate, :createdAt, :updatedAt], - do: "—" + do: nil defp format_receipt_cell(col_key, val) when col_key in [:bookingDate, :createdAt, :updatedAt] do format_receipt_date(val) @@ -1253,7 +1263,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do defp translate_receipt_status("draft"), do: gettext("Draft") defp translate_receipt_status("incompleted"), do: gettext("Incompleted") defp translate_receipt_status("completed"), do: gettext("Completed") - defp translate_receipt_status("empty"), do: "—" + defp translate_receipt_status("empty"), do: nil defp translate_receipt_status(other), do: other # Translate API receipt type values (extend as API returns more values) diff --git a/lib/mv_web/live/membership_fee_settings_live.ex b/lib/mv_web/live/membership_fee_settings_live.ex index 94e64c7..db044b5 100644 --- a/lib/mv_web/live/membership_fee_settings_live.ex +++ b/lib/mv_web/live/membership_fee_settings_live.ex @@ -329,7 +329,7 @@ defmodule MvWeb.MembershipFeeSettingsLive do <:col :let={mft} label={gettext("Members")}> - <.badge variant="neutral">{get_member_count(mft, @member_counts)} + {get_member_count(mft, @member_counts)} <:action :let={mft}> diff --git a/lib/mv_web/live/role_live/index.html.heex b/lib/mv_web/live/role_live/index.html.heex index 94d1fc6..bb61bb1 100644 --- a/lib/mv_web/live/role_live/index.html.heex +++ b/lib/mv_web/live/role_live/index.html.heex @@ -53,7 +53,7 @@ <:col :let={role} label={gettext("Users")}> - <.badge variant="neutral">{get_user_count(role, @user_counts)} + {get_user_count(role, @user_counts)} diff --git a/lib/mv_web/live/user_live/index.html.heex b/lib/mv_web/live/user_live/index.html.heex index 91a5485..c84f258 100644 --- a/lib/mv_web/live/user_live/index.html.heex +++ b/lib/mv_web/live/user_live/index.html.heex @@ -38,25 +38,25 @@ {user.role.name} <:col :let={user} label={gettext("Linked Member")}> - <%= if user.member do %> + <.maybe_value value={user.member} empty_sr_text={gettext("No member linked")}> {MvWeb.Helpers.MemberHelpers.display_name(user.member)} - <% else %> - {gettext("No member linked")} - <% end %> + <:col :let={user} label={gettext("Password")}> - <%= if MvWeb.Helpers.UserHelpers.has_password?(user) do %> + <.maybe_value + value={MvWeb.Helpers.UserHelpers.has_password?(user)} + empty_sr_text={gettext("Not set")} + > {gettext("Enabled")} - <% else %> - - <% end %> + <:col :let={user} label={gettext("OIDC")}> - <%= if MvWeb.Helpers.UserHelpers.has_oidc?(user) do %> + <.maybe_value + value={MvWeb.Helpers.UserHelpers.has_oidc?(user)} + empty_sr_text={gettext("Not set")} + > {gettext("Linked")} - <% else %> - - <% end %> + diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 00d5541..32409ea 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -591,18 +591,6 @@ msgstr "Diese E-Mail-Adresse ist bereits mit einem anderen OIDC-Konto verknüpft msgid "Custom Fields" msgstr "Benutzerdefinierte Felder" -#: lib/mv_web/live/custom_field_live/index_component.ex -#, elixir-autogen, elixir-format -msgid "%{count} member has a value assigned for this custom field." -msgid_plural "%{count} members have values assigned for this custom field." -msgstr[0] "%{count} Mitglied hat einen Wert für dieses benutzerdefinierte Feld zugewiesen." -msgstr[1] "%{count} Mitglieder haben Werte für dieses benutzerdefinierte Feld zugewiesen." - -#: lib/mv_web/live/custom_field_live/index_component.ex -#, elixir-autogen, elixir-format -msgid "All custom field values will be permanently deleted when you delete this custom field." -msgstr "Alle benutzerdefinierten Feldwerte werden beim Löschen dieses benutzerdefinierten Feldes dauerhaft gelöscht." - #: lib/mv_web/live/custom_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Enter the text above to confirm" @@ -788,6 +776,7 @@ msgstr "Beitragsdaten" msgid "Payments" msgstr "Zahlungen" +#: lib/mv_web/live/datafields_live.ex #: lib/mv_web/live/member_live/form.ex #: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format @@ -1386,6 +1375,8 @@ msgid "None (no default)" msgstr "Keine (kein Standard)" #: lib/mv_web/live/member_live/show.ex +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#: lib/mv_web/live/user_live/index.html.heex #, elixir-autogen, elixir-format msgid "Not set" msgstr "Nicht gesetzt" @@ -2918,11 +2909,6 @@ msgstr "Client-ID" msgid "Client Secret" msgstr "Client-Geheimnis" -#: lib/mv_web/live/datafields_live.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Custom fields" -msgstr "Benutzerdefinierte Felder" - #: lib/mv_web/components/layouts/sidebar.ex #: lib/mv_web/live/datafields_live.ex #, elixir-autogen, elixir-format @@ -2964,11 +2950,6 @@ msgstr "Aus OIDC_REDIRECT_URI" msgid "Groups claim" msgstr "Gruppenclaim" -#: lib/mv_web/live/datafields_live.ex -#, elixir-autogen, elixir-format -msgid "Member fields" -msgstr "Mitgliedsfelder" - #: lib/mv_web/components/layouts/sidebar.ex #, elixir-autogen, elixir-format, fuzzy msgid "Membership fee settings" @@ -3225,128 +3206,32 @@ msgstr "Verwalte welche Daten du für eure Mitglieder speichern möchtest. Lege msgid "Manage users and their permissions." msgstr "Verwalte Benutzer*innen und ihre Berechtigungen." -#~ #: lib/mv_web/live/member_field_live/form_component.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Back to Settings" -#~ msgstr "Zurück zu den Einstellungen" +#: lib/mv_web/live/custom_field_live/index_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "%{count} member has a value assigned for this datafield." +msgid_plural "%{count} members have values assigned for this datafield." +msgstr[0] "%{count} Mitglied hat einen Wert für dieses benutzerdefinierte Feld zugewiesen." +msgstr[1] "%{count} Mitglieder haben Werte für dieses benutzerdefinierte Feld zugewiesen." -#~ #: lib/mv_web/live/role_live/index.html.heex -#~ #, elixir-autogen, elixir-format -#~ msgid "Cannot delete system role" -#~ msgstr "System-Rolle kann nicht gelöscht werden" +#: lib/mv_web/live/datafields_live.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Individual Datafields" +msgstr "Individuelle Datenfelder" -#~ #: lib/mv_web/live/custom_field_live/index_component.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Click for custom field details" -#~ msgstr "Klicke für Datenfeld-Details" +#: lib/mv_web/live/member_live/index.html.heex +#, elixir-autogen, elixir-format, fuzzy +msgid "No group assignment" +msgstr "Keine Gruppenzuordnung" -#~ #: lib/mv_web/live/member_field_live/index_component.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Click for datafield details" -#~ msgstr "Klicke für Datenfeld-Details" +#: lib/mv_web/components/core_components.ex +#: lib/mv_web/live/group_live/index.ex +#: lib/mv_web/live/member_live/index.html.heex +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Not specified" +msgstr "Nicht angegeben" -#~ #: lib/mv_web/live/member_live/form.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Coming soon" -#~ msgstr "Demnächst verfügbar" - -#~ #: lib/mv_web/live/membership_fee_settings_live.ex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "Configure global settings and fee types for membership fees." -#~ msgstr "Globale Einstellungen für Mitgliedsbeiträge konfigurieren." - -#~ #: lib/mv_web/live/datafields_live.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Configure member fields and custom data fields." -#~ msgstr "Mitgliedsfelder und benutzerdefinierte Datenfelder konfigurieren." - -#~ #: lib/mv_web/live/components/field_visibility_dropdown_component.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Custom Field %{id}" -#~ msgstr "Benutzerdefiniertes Feld %{id}" - -#~ #: lib/mv_web/live/import_live.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Datei auswählen" -#~ msgstr "Datei auswählen" - -#~ #: lib/mv_web/live/custom_field_live/index_component.ex -#~ #: lib/mv_web/live/member_field_live/index_component.ex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "Edit datafield" -#~ msgstr "Datenfeld bearbeiten" - -#~ #: lib/mv_web/live/user_live/index.html.heex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "Edit user" -#~ msgstr "Benutzer*in bearbeiten" - -#~ #: lib/mv_web/live/statistics_live.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Overview from first membership to today" -#~ msgstr "Übersicht vom ersten Eintritt bis heute" - -#~ #: lib/mv_web/live/components/member_filter_component.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Reset" -#~ msgstr "Zurücksetzen" - -#~ #: lib/mv_web/live/role_live/show.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Rolle bearbeiten" -#~ msgstr "Rolle bearbeiten" - -#~ #: lib/mv_web/live/role_live/form.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Save Role" -#~ msgstr "Rolle speichern" - -#~ #: lib/mv_web/live/user_live/index.html.heex -#~ #, elixir-autogen, elixir-format -#~ msgid "Select all users" -#~ msgstr "Alle Benutzer*innen auswählen" - -#~ #: lib/mv_web/live/user_live/index.html.heex -#~ #, elixir-autogen, elixir-format -#~ msgid "Select user" -#~ msgstr "Benutzer*in auswählen" - -#~ #: lib/mv_web/live/role_live/index.html.heex -#~ #, elixir-autogen, elixir-format -#~ msgid "System roles cannot be deleted" -#~ msgstr "System-Rollen können nicht gelöscht werden" - -#~ #: lib/mv_web/live/user_live/show.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "This is a user record from your database." -#~ msgstr "Dies ist ein Benutzer*innen-Datensatz aus Ihrer Datenbank." - -#~ #: lib/mv_web/live/membership_fee_type_live/form.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Use this form to manage membership fee types in your database." -#~ msgstr "Verwende dieses Formular, um Mitgliedsbeitragsarten in deiner Datenbank zu verwalten." - -#~ #: lib/mv_web/live/role_live/form.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Use this form to manage roles in your database." -#~ msgstr "Verwende dieses Formular, um Rollen in deiner Datenbank zu verwalten." - -#~ #: lib/mv_web/live/user_live/form.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Use this form to manage user records in your database." -#~ msgstr "Verwende dieses Formular, um Benutzer*innen-Datensätze zu verwalten." - -#~ #: lib/mv_web/live/group_live/index.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "View" -#~ msgstr "Anzeigen" - -#~ #: lib/mv_web/live/member_live/index.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "You do not have permission to access this member" -#~ msgstr "Du hast keine Berechtigung, auf dieses Mitglied zuzugreifen" - -#~ #: lib/mv_web/live/user_live/index.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "You do not have permission to access this user" -#~ msgstr "Du hast keine Berechtigung, auf diese*n Benutzer*in zuzugreifen" +#: lib/mv_web/live/custom_field_live/index_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "All datafield values will be permanently deleted when you delete this datafield." +msgstr "Alle benutzerdefinierten Feldwerte werden beim Löschen dieses benutzerdefinierten Feldes dauerhaft gelöscht." diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index ec97119..6b80e27 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -592,18 +592,6 @@ msgstr "" msgid "Custom Fields" msgstr "" -#: lib/mv_web/live/custom_field_live/index_component.ex -#, elixir-autogen, elixir-format -msgid "%{count} member has a value assigned for this custom field." -msgid_plural "%{count} members have values assigned for this custom field." -msgstr[0] "" -msgstr[1] "" - -#: lib/mv_web/live/custom_field_live/index_component.ex -#, elixir-autogen, elixir-format -msgid "All custom field values will be permanently deleted when you delete this custom field." -msgstr "" - #: lib/mv_web/live/custom_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Enter the text above to confirm" @@ -789,6 +777,7 @@ msgstr "" msgid "Payments" msgstr "" +#: lib/mv_web/live/datafields_live.ex #: lib/mv_web/live/member_live/form.ex #: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format @@ -1387,6 +1376,8 @@ msgid "None (no default)" msgstr "" #: lib/mv_web/live/member_live/show.ex +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#: lib/mv_web/live/user_live/index.html.heex #, elixir-autogen, elixir-format msgid "Not set" msgstr "" @@ -2918,11 +2909,6 @@ msgstr "" msgid "Client Secret" msgstr "" -#: lib/mv_web/live/datafields_live.ex -#, elixir-autogen, elixir-format -msgid "Custom fields" -msgstr "" - #: lib/mv_web/components/layouts/sidebar.ex #: lib/mv_web/live/datafields_live.ex #, elixir-autogen, elixir-format @@ -2964,11 +2950,6 @@ msgstr "" msgid "Groups claim" msgstr "" -#: lib/mv_web/live/datafields_live.ex -#, elixir-autogen, elixir-format -msgid "Member fields" -msgstr "" - #: lib/mv_web/components/layouts/sidebar.ex #, elixir-autogen, elixir-format msgid "Membership fee settings" @@ -3224,3 +3205,33 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Manage users and their permissions." msgstr "" + +#: lib/mv_web/live/custom_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "%{count} member has a value assigned for this datafield." +msgid_plural "%{count} members have values assigned for this datafield." +msgstr[0] "" +msgstr[1] "" + +#: lib/mv_web/live/datafields_live.ex +#, elixir-autogen, elixir-format +msgid "Individual Datafields" +msgstr "" + +#: lib/mv_web/live/member_live/index.html.heex +#, elixir-autogen, elixir-format +msgid "No group assignment" +msgstr "" + +#: lib/mv_web/components/core_components.ex +#: lib/mv_web/live/group_live/index.ex +#: lib/mv_web/live/member_live/index.html.heex +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Not specified" +msgstr "" + +#: lib/mv_web/live/custom_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "All datafield values will be permanently deleted when you delete this datafield." +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index a0c41cc..2ea6be5 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -592,18 +592,6 @@ msgstr "" msgid "Custom Fields" msgstr "" -#: lib/mv_web/live/custom_field_live/index_component.ex -#, elixir-autogen, elixir-format -msgid "%{count} member has a value assigned for this custom field." -msgid_plural "%{count} members have values assigned for this custom field." -msgstr[0] "" -msgstr[1] "" - -#: lib/mv_web/live/custom_field_live/index_component.ex -#, elixir-autogen, elixir-format -msgid "All custom field values will be permanently deleted when you delete this custom field." -msgstr "" - #: lib/mv_web/live/custom_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Enter the text above to confirm" @@ -789,6 +777,7 @@ msgstr "" msgid "Payments" msgstr "" +#: lib/mv_web/live/datafields_live.ex #: lib/mv_web/live/member_live/form.ex #: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format @@ -1387,6 +1376,8 @@ msgid "None (no default)" msgstr "" #: lib/mv_web/live/member_live/show.ex +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#: lib/mv_web/live/user_live/index.html.heex #, elixir-autogen, elixir-format, fuzzy msgid "Not set" msgstr "" @@ -2918,11 +2909,6 @@ msgstr "" msgid "Client Secret" msgstr "" -#: lib/mv_web/live/datafields_live.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Custom fields" -msgstr "" - #: lib/mv_web/components/layouts/sidebar.ex #: lib/mv_web/live/datafields_live.ex #, elixir-autogen, elixir-format @@ -2964,11 +2950,6 @@ msgstr "" msgid "Groups claim" msgstr "" -#: lib/mv_web/live/datafields_live.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Member fields" -msgstr "" - #: lib/mv_web/components/layouts/sidebar.ex #, elixir-autogen, elixir-format, fuzzy msgid "Membership fee settings" @@ -3225,123 +3206,32 @@ msgstr "" msgid "Manage users and their permissions." msgstr "" -#~ #: lib/mv_web/live/member_field_live/form_component.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Back to Settings" -#~ msgstr "" +#: lib/mv_web/live/custom_field_live/index_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "%{count} member has a value assigned for this datafield." +msgid_plural "%{count} members have values assigned for this datafield." +msgstr[0] "" +msgstr[1] "" -#~ #: lib/mv_web/live/role_live/index.html.heex -#~ #, elixir-autogen, elixir-format -#~ msgid "Cannot delete system role" -#~ msgstr "" +#: lib/mv_web/live/datafields_live.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Individual Datafields" +msgstr "" -#~ #: lib/mv_web/live/custom_field_live/index_component.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Click for custom field details" -#~ msgstr "" +#: lib/mv_web/live/member_live/index.html.heex +#, elixir-autogen, elixir-format, fuzzy +msgid "No group assignment" +msgstr "" -#~ #: lib/mv_web/live/member_field_live/index_component.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Click for datafield details" -#~ msgstr "" +#: lib/mv_web/components/core_components.ex +#: lib/mv_web/live/group_live/index.ex +#: lib/mv_web/live/member_live/index.html.heex +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Not specified" +msgstr "" -#~ #: lib/mv_web/live/member_live/form.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Coming soon" -#~ msgstr "" - -#~ #: lib/mv_web/live/membership_fee_settings_live.ex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "Configure global settings and fee types for membership fees." -#~ msgstr "" - -#~ #: lib/mv_web/live/datafields_live.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Configure member fields and custom data fields." -#~ msgstr "" - -#~ #: lib/mv_web/live/components/field_visibility_dropdown_component.ex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "Custom Field %{id}" -#~ msgstr "" - -#~ #: lib/mv_web/live/custom_field_live/index_component.ex -#~ #: lib/mv_web/live/member_field_live/index_component.ex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "Edit datafield" -#~ msgstr "" - -#~ #: lib/mv_web/live/user_live/index.html.heex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "Edit user" -#~ msgstr "" - -#~ #: lib/mv_web/live/statistics_live.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Overview from first membership to today" -#~ msgstr "" - -#~ #: lib/mv_web/live/components/member_filter_component.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Reset" -#~ msgstr "" - -#~ #: lib/mv_web/live/role_live/show.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Rolle bearbeiten" -#~ msgstr "" - -#~ #: lib/mv_web/live/role_live/form.ex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "Save Role" -#~ msgstr "" - -#~ #: lib/mv_web/live/user_live/index.html.heex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "Select all users" -#~ msgstr "" - -#~ #: lib/mv_web/live/user_live/index.html.heex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "Select user" -#~ msgstr "" - -#~ #: lib/mv_web/live/role_live/index.html.heex -#~ #, elixir-autogen, elixir-format -#~ msgid "System roles cannot be deleted" -#~ msgstr "" - -#~ #: lib/mv_web/live/user_live/show.ex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "This is a user record from your database." -#~ msgstr "" - -#~ #: lib/mv_web/live/membership_fee_type_live/form.ex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "Use this form to manage membership fee types in your database." -#~ msgstr "" - -#~ #: lib/mv_web/live/role_live/form.ex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "Use this form to manage roles in your database." -#~ msgstr "" - -#~ #: lib/mv_web/live/user_live/form.ex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "Use this form to manage user records in your database." -#~ msgstr "" - -#~ #: lib/mv_web/live/group_live/index.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "View" -#~ msgstr "" - -#~ #: lib/mv_web/live/member_live/index.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "You do not have permission to access this member" -#~ msgstr "" - -#~ #: lib/mv_web/live/user_live/index.ex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "You do not have permission to access this user" -#~ msgstr "" +#: lib/mv_web/live/custom_field_live/index_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "All datafield values will be permanently deleted when you delete this datafield." +msgstr "" diff --git a/test/mv_web/live/custom_field_live/deletion_test.exs b/test/mv_web/live/custom_field_live/deletion_test.exs index 759ca1d..e841120 100644 --- a/test/mv_web/live/custom_field_live/deletion_test.exs +++ b/test/mv_web/live/custom_field_live/deletion_test.exs @@ -76,7 +76,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do refute has_element?(view, "h2", "Custom fields") # 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 datafield" # Should show the slug assert render(view) =~ custom_field.slug @@ -95,7 +95,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do open_delete_modal(view, custom_field) # Should show plural form - assert render(view) =~ "2 members have values assigned for this custom field" + assert render(view) =~ "2 members have values assigned for this datafield" end test "shows 0 members for custom field without values", %{conn: conn} do @@ -105,7 +105,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do open_delete_modal(view, custom_field) # Should show 0 members - assert render(view) =~ "0 members have values assigned for this custom field" + assert render(view) =~ "0 members have values assigned for this datafield" end end diff --git a/test/mv_web/member_live/index_groups_display_test.exs b/test/mv_web/member_live/index_groups_display_test.exs index b28b978..263ac2a 100644 --- a/test/mv_web/member_live/index_groups_display_test.exs +++ b/test/mv_web/member_live/index_groups_display_test.exs @@ -95,6 +95,20 @@ defmodule MvWeb.MemberLive.IndexGroupsDisplayTest do assert html =~ member3.first_name end + test "empty group cell is visually empty with sr-only text (no dash)", %{ + conn: conn, + member3: member3 + } do + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, "/members") + assert html =~ member3.first_name + # Screen reader gets a meaningful label for the empty cell + assert html =~ "sr-only" + assert html =~ "No group assignment" + # No visible dash as placeholder (Design Guidelines §8.6) + refute html =~ ~r/]*class="[^"]*text-base-content\/50[^"]*"[^>]*>—<\/span>/ + end + test "displays group name correctly in badge", %{conn: conn, group1: group1} do conn = conn_with_oidc_user(conn) {:ok, _view, html} = live(conn, "/members") diff --git a/test/mv_web/user_live/index_test.exs b/test/mv_web/user_live/index_test.exs index 596d02d..c0be795 100644 --- a/test/mv_web/user_live/index_test.exs +++ b/test/mv_web/user_live/index_test.exs @@ -210,7 +210,9 @@ defmodule MvWeb.UserLive.IndexTest do end describe "Password column display" do - test "user without password shows em dash in Password column", %{conn: conn} do + test "user without password shows empty cell with sr-only text in Password column", %{ + conn: conn + } do # User created with hashed_password: nil (no password) - must not get default password user_no_pw = create_test_user(%{ @@ -223,9 +225,13 @@ defmodule MvWeb.UserLive.IndexTest do assert html =~ "no-password@example.com" - # Password column must show "—" (em dash) for user without password, not "Enabled" + # Password column: visually empty, screen-reader gets "Not set" (Design Guidelines §8.6) row = view |> element("tr#row-#{user_no_pw.id}") |> render() - assert row =~ "—", "Password column should show em dash for user without password" + assert row =~ "sr-only", "Password column should have sr-only text for accessibility" + assert row =~ "Not set", "Screen reader should get 'Not set' for empty password" + + refute row =~ "—", + "Password column must not show dash (use empty cell + sr-only per CODE_GUIDELINES §8)" refute row =~ "Enabled", "Password column must not show Enabled when user has no password"