Adds more consistency in various UX topics closes #447 #448

Merged
carla merged 9 commits from feat/447_concistency into main 2026-02-25 17:34:12 +01:00
8 changed files with 411 additions and 324 deletions
Showing only changes of commit e5a6003ace - Show all commits

View file

@ -272,6 +272,11 @@ Notes:
- **MUST:** Truncate long values consistently (same max widths for name/email-like fields). - **MUST:** Truncate long values consistently (same max widths for name/email-like fields).
- **MUST:** Tooltip reveals full value when truncated. - **MUST:** Tooltip reveals full value when truncated.
### 8.5 Loading/Lists/Tables: keep filters visible on desktop
- On **desktop (lg: breakpoint)** only: list pages with large datasets (e.g. Members overview) keep the page header and filter/search bar visible while the user scrolls. Only the table body scrolls inside a constrained area (`lg:max-h-[calc(100vh-<offset>)] lg:overflow-auto`). This preserves context and avoids losing filters when scrolling.
- 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 tables `sticky_header={true}` so the tables `<thead>` 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.
--- ---
## 9) Flash / Toast messages (mandatory UX) ## 9) Flash / Toast messages (mandatory UX)

View file

@ -715,6 +715,11 @@ defmodule MvWeb.CoreComponents do
attr :sort_field, :any, default: nil, doc: "current sort field" attr :sort_field, :any, default: nil, doc: "current sort field"
attr :sort_order, :atom, default: nil, doc: "current sort order" attr :sort_order, :atom, default: nil, doc: "current sort order"
attr :sticky_header, :boolean,
default: false,
doc:
"when true, thead th get lg:sticky lg:top-0 bg-base-100 z-10 for use inside a scroll container on desktop"
slot :col, required: true do slot :col, required: true do
attr :label, :string attr :label, :string
attr :class, :string attr :class, :string
@ -745,12 +750,12 @@ defmodule MvWeb.CoreComponents do
<tr> <tr>
<th <th
:for={col <- @col} :for={col <- @col}
class={Map.get(col, :class)} class={table_th_class(col, @sticky_header)}
aria-sort={table_th_aria_sort(col, @sort_field, @sort_order)} aria-sort={table_th_aria_sort(col, @sort_field, @sort_order)}
> >
{col[:label]} {col[:label]}
</th> </th>
<th :for={dyn_col <- @dynamic_cols}> <th :for={dyn_col <- @dynamic_cols} class={table_th_sticky_class(@sticky_header)}>
<.live_component <.live_component
module={MvWeb.Components.SortHeaderComponent} module={MvWeb.Components.SortHeaderComponent}
id={:"sort_custom_field_#{dyn_col[:custom_field].id}"} id={:"sort_custom_field_#{dyn_col[:custom_field].id}"}
@ -760,7 +765,7 @@ defmodule MvWeb.CoreComponents do
sort_order={@sort_order} sort_order={@sort_order}
/> />
</th> </th>
<th :if={@action != []}> <th :if={@action != []} class={table_th_sticky_class(@sticky_header)}>
<span class="sr-only">{gettext("Actions")}</span> <span class="sr-only">{gettext("Actions")}</span>
</th> </th>
</tr> </tr>
@ -891,6 +896,18 @@ defmodule MvWeb.CoreComponents do
end end
end end
# Combines column class with optional sticky header classes (desktop only; theme-friendly bg).
defp table_th_class(col, sticky_header) do
base = Map.get(col, :class)
sticky = if sticky_header, do: "lg:sticky lg:top-0 bg-base-100 z-10", else: nil
[base, sticky] |> Enum.filter(& &1) |> Enum.join(" ")
end
defp table_th_sticky_class(true),
do: "lg:sticky lg:top-0 bg-base-100 z-10"
defp table_th_sticky_class(_), do: nil
@doc """ @doc """
Renders a data list. Renders a data list.

View file

@ -90,302 +90,311 @@
/> />
</div> </div>
<.table <%!-- On desktop (lg:), only the table area scrolls; header and filters stay visible. On mobile, normal flow. --%>
id="members" <div
rows={@members} class="lg:max-h-[calc(100vh-14rem)] lg:overflow-auto min-h-0"
row_id={fn member -> "row-#{member.id}" end} data-testid="members-table-scroll"
row_click={fn member -> JS.push("select_row_and_navigate", value: %{id: member.id}) end} role="region"
row_tooltip={gettext("Click for member details")} aria-label={gettext("Members table")}
row_selected?={fn member -> MapSet.member?(@selected_members, member.id) end}
dynamic_cols={@dynamic_cols}
sort_field={@sort_field}
sort_order={@sort_order}
> >
<.table
id="members"
rows={@members}
sticky_header={true}
row_id={fn member -> "row-#{member.id}" end}
row_click={fn member -> JS.push("select_row_and_navigate", value: %{id: member.id}) end}
row_tooltip={gettext("Click for member details")}
row_selected?={fn member -> MapSet.member?(@selected_members, member.id) end}
dynamic_cols={@dynamic_cols}
sort_field={@sort_field}
sort_order={@sort_order}
>
<!-- <:col :let={member} label="Id">{member.id}</:col> --> <!-- <:col :let={member} label="Id">{member.id}</:col> -->
<:col <:col
:let={member} :let={member}
col_click={&MvWeb.MemberLive.Index.checkbox_column_click/1} col_click={&MvWeb.MemberLive.Index.checkbox_column_click/1}
label={ label={
~H""" ~H"""
<.input
type="checkbox"
name="select_all"
phx-click="select_all"
checked={MapSet.equal?(@selected_members, @members |> Enum.map(& &1.id) |> MapSet.new())}
aria-label={gettext("Select all members")}
role="checkbox"
/>
"""
}
>
<.input <.input
type="checkbox" type="checkbox"
name="select_all" name={member.id}
phx-click="select_all" checked={MapSet.member?(@selected_members, member.id)}
checked={MapSet.equal?(@selected_members, @members |> Enum.map(& &1.id) |> MapSet.new())} aria-label={gettext("Select member")}
aria-label={gettext("Select all members")}
role="checkbox" role="checkbox"
/> />
""" </:col>
} <:col
> :let={member}
<.input :if={:first_name in @member_fields_visible}
type="checkbox" label={
name={member.id} ~H"""
checked={MapSet.member?(@selected_members, member.id)} <.live_component
aria-label={gettext("Select member")} module={MvWeb.Components.SortHeaderComponent}
role="checkbox" id={:sort_first_name}
/> field={:first_name}
</:col> label={gettext("First name")}
<:col sort_field={@sort_field}
:let={member} sort_order={@sort_order}
:if={:first_name in @member_fields_visible} />
label={ """
~H""" }
<.live_component >
module={MvWeb.Components.SortHeaderComponent} {member.first_name}
id={:sort_first_name} </:col>
field={:first_name} <:col
label={gettext("First name")} :let={member}
sort_field={@sort_field} :if={:last_name in @member_fields_visible}
sort_order={@sort_order} label={
/> ~H"""
""" <.live_component
} module={MvWeb.Components.SortHeaderComponent}
> id={:sort_last_name}
{member.first_name} field={:last_name}
</:col> label={gettext("Last name")}
<:col sort_field={@sort_field}
:let={member} sort_order={@sort_order}
:if={:last_name in @member_fields_visible} />
label={ """
~H""" }
<.live_component >
module={MvWeb.Components.SortHeaderComponent} {member.last_name}
id={:sort_last_name} </:col>
field={:last_name} <:col
label={gettext("Last name")} :let={member}
sort_field={@sort_field} :if={:email in @member_fields_visible}
sort_order={@sort_order} label={
/> ~H"""
""" <.live_component
} module={MvWeb.Components.SortHeaderComponent}
> id={:sort_email}
{member.last_name} field={:email}
</:col> label={gettext("Email")}
<:col sort_field={@sort_field}
:let={member} sort_order={@sort_order}
:if={:email in @member_fields_visible} />
label={ """
~H""" }
<.live_component >
module={MvWeb.Components.SortHeaderComponent} {member.email}
id={:sort_email} </:col>
field={:email} <:col
label={gettext("Email")} :let={member}
sort_field={@sort_field} :if={:join_date in @member_fields_visible}
sort_order={@sort_order} label={
/> ~H"""
""" <.live_component
} module={MvWeb.Components.SortHeaderComponent}
> id={:sort_join_date}
{member.email} field={:join_date}
</:col> label={gettext("Join Date")}
<:col sort_field={@sort_field}
:let={member} sort_order={@sort_order}
:if={:join_date in @member_fields_visible} />
label={ """
~H""" }
<.live_component >
module={MvWeb.Components.SortHeaderComponent} {MvWeb.MemberLive.Index.format_date(member.join_date)}
id={:sort_join_date} </:col>
field={:join_date} <:col
label={gettext("Join Date")} :let={member}
sort_field={@sort_field} :if={:exit_date in @member_fields_visible}
sort_order={@sort_order} label={
/> ~H"""
""" <.live_component
} module={MvWeb.Components.SortHeaderComponent}
> id={:sort_exit_date}
{MvWeb.MemberLive.Index.format_date(member.join_date)} field={:exit_date}
</:col> label={gettext("Exit Date")}
<:col sort_field={@sort_field}
:let={member} sort_order={@sort_order}
:if={:exit_date in @member_fields_visible} />
label={ """
~H""" }
<.live_component >
module={MvWeb.Components.SortHeaderComponent} {MvWeb.MemberLive.Index.format_date(member.exit_date)}
id={:sort_exit_date} </:col>
field={:exit_date} <:col
label={gettext("Exit Date")} :let={member}
sort_field={@sort_field} :if={:notes in @member_fields_visible}
sort_order={@sort_order} label={gettext("Notes")}
/> >
""" {member.notes}
} </:col>
> <:col
{MvWeb.MemberLive.Index.format_date(member.exit_date)} :let={member}
</:col> :if={:city in @member_fields_visible}
<:col label={
:let={member} ~H"""
:if={:notes in @member_fields_visible} <.live_component
label={gettext("Notes")} module={MvWeb.Components.SortHeaderComponent}
> id={:sort_city}
{member.notes} field={:city}
</:col> label={gettext("City")}
<:col sort_field={@sort_field}
:let={member} sort_order={@sort_order}
:if={:city in @member_fields_visible} />
label={ """
~H""" }
<.live_component >
module={MvWeb.Components.SortHeaderComponent} {member.city}
id={:sort_city} </:col>
field={:city} <:col
label={gettext("City")} :let={member}
sort_field={@sort_field} :if={:street in @member_fields_visible}
sort_order={@sort_order} label={
/> ~H"""
""" <.live_component
} module={MvWeb.Components.SortHeaderComponent}
> id={:sort_street}
{member.city} field={:street}
</:col> label={gettext("Street")}
<:col sort_field={@sort_field}
:let={member} sort_order={@sort_order}
:if={:street in @member_fields_visible} />
label={ """
~H""" }
<.live_component >
module={MvWeb.Components.SortHeaderComponent} {member.street}
id={:sort_street} </:col>
field={:street} <:col
label={gettext("Street")} :let={member}
sort_field={@sort_field} :if={:house_number in @member_fields_visible}
sort_order={@sort_order} label={
/> ~H"""
""" <.live_component
} module={MvWeb.Components.SortHeaderComponent}
> id={:sort_house_number}
{member.street} field={:house_number}
</:col> label={gettext("House Number")}
<:col sort_field={@sort_field}
:let={member} sort_order={@sort_order}
:if={:house_number in @member_fields_visible} />
label={ """
~H""" }
<.live_component >
module={MvWeb.Components.SortHeaderComponent} {member.house_number}
id={:sort_house_number} </:col>
field={:house_number} <:col
label={gettext("House Number")} :let={member}
sort_field={@sort_field} :if={:postal_code in @member_fields_visible}
sort_order={@sort_order} label={
/> ~H"""
""" <.live_component
} module={MvWeb.Components.SortHeaderComponent}
> id={:sort_postal_code}
{member.house_number} field={:postal_code}
</:col> label={gettext("Postal Code")}
<:col sort_field={@sort_field}
:let={member} sort_order={@sort_order}
:if={:postal_code in @member_fields_visible} />
label={ """
~H""" }
<.live_component >
module={MvWeb.Components.SortHeaderComponent} {member.postal_code}
id={:sort_postal_code} </:col>
field={:postal_code} <:col
label={gettext("Postal Code")} :let={member}
sort_field={@sort_field} :if={:membership_fee_start_date in @member_fields_visible}
sort_order={@sort_order} label={
/> ~H"""
""" <.live_component
} module={MvWeb.Components.SortHeaderComponent}
> id={:sort_membership_fee_start_date}
{member.postal_code} field={:membership_fee_start_date}
</:col> label={gettext("Membership Fee Start Date")}
<:col sort_field={@sort_field}
:let={member} sort_order={@sort_order}
:if={:membership_fee_start_date in @member_fields_visible} />
label={ """
~H""" }
<.live_component >
module={MvWeb.Components.SortHeaderComponent} {MvWeb.MemberLive.Index.format_date(member.membership_fee_start_date)}
id={:sort_membership_fee_start_date} </:col>
field={:membership_fee_start_date} <:col
label={gettext("Membership Fee Start Date")} :let={member}
sort_field={@sort_field} :if={:membership_fee_type in @member_fields_visible}
sort_order={@sort_order} label={
/> ~H"""
""" <.live_component
} module={MvWeb.Components.SortHeaderComponent}
> id={:sort_membership_fee_type}
{MvWeb.MemberLive.Index.format_date(member.membership_fee_start_date)} field={:membership_fee_type}
</:col> label={gettext("Fee Type")}
<:col sort_field={@sort_field}
:let={member} sort_order={@sort_order}
:if={:membership_fee_type in @member_fields_visible} />
label={ """
~H""" }
<.live_component >
module={MvWeb.Components.SortHeaderComponent} <%= if member.membership_fee_type do %>
id={:sort_membership_fee_type} {member.membership_fee_type.name}
field={:membership_fee_type} <% else %>
label={gettext("Fee Type")} <span class="text-base-content/50">—</span>
sort_field={@sort_field} <% end %>
sort_order={@sort_order} </:col>
/> <:col
""" :let={member}
} :if={:membership_fee_status in @member_fields_visible}
> label={gettext("Membership Fee Status")}
<%= if member.membership_fee_type do %> >
{member.membership_fee_type.name} <%= if badge = MvWeb.MemberLive.Index.MembershipFeeStatus.format_cycle_status_badge(
<% else %>
<span class="text-base-content/50">—</span>
<% end %>
</:col>
<:col
:let={member}
:if={:membership_fee_status in @member_fields_visible}
label={gettext("Membership Fee Status")}
>
<%= if badge = MvWeb.MemberLive.Index.MembershipFeeStatus.format_cycle_status_badge(
MvWeb.MemberLive.Index.MembershipFeeStatus.get_cycle_status_for_member(member, @show_current_cycle) MvWeb.MemberLive.Index.MembershipFeeStatus.get_cycle_status_for_member(member, @show_current_cycle)
) do %> ) do %>
<span class={["badge", badge.color]}> <span class={["badge", badge.color]}>
<.icon name={badge.icon} class="size-4" /> <.icon name={badge.icon} class="size-4" />
{badge.label} {badge.label}
</span> </span>
<% else %> <% else %>
<span class="badge badge-ghost">{gettext("No cycle")}</span> <span class="badge badge-ghost">{gettext("No cycle")}</span>
<% end %> <% end %>
</:col> </:col>
<:col <:col
:let={member} :let={member}
:if={:groups in @member_fields_visible} :if={:groups in @member_fields_visible}
label={ label={
~H""" ~H"""
<.live_component <.live_component
module={MvWeb.Components.SortHeaderComponent} module={MvWeb.Components.SortHeaderComponent}
id={:sort_groups} id={:sort_groups}
field={:groups} field={:groups}
label={gettext("Groups")} label={gettext("Groups")}
sort_field={@sort_field} sort_field={@sort_field}
sort_order={@sort_order} sort_order={@sort_order}
/> />
""" """
} }
> >
<%= for group <- (member.groups || []) do %> <%= for group <- (member.groups || []) do %>
<span <span
class="badge badge-outline badge-primary" class="badge badge-outline badge-primary"
aria-label={gettext("Member of group %{name}", name: group.name)} aria-label={gettext("Member of group %{name}", name: group.name)}
> >
{group.name} {group.name}
</span> </span>
<% end %> <% end %>
<%= if (member.groups || []) == [] do %> <%= if (member.groups || []) == [] do %>
<span class="text-base-content/50">—</span> <span class="text-base-content/50">—</span>
<% end %> <% end %>
</:col> </:col>
<:action :let={member}> <:action :let={member}>
<div class="sr-only"> <div class="sr-only">
<.link navigate={~p"/members/#{member}"} data-testid="member-show-link"> <.link navigate={~p"/members/#{member}"} data-testid="member-show-link">
{gettext("Show")} {gettext("Show")}
</.link> </.link>
</div> </div>
</:action> </:action>
</.table> </.table>
</div>
</Layouts.app> </Layouts.app>

View file

@ -174,7 +174,11 @@ defmodule MvWeb.RoleLive.Show do
{gettext("Back")} {gettext("Back")}
</.button> </.button>
<%= if can?(@current_user, :update, Mv.Authorization.Role) do %> <%= if can?(@current_user, :update, Mv.Authorization.Role) do %>
<.button variant="primary" navigate={~p"/admin/roles/#{@role}/edit"} data-testid=role-edit"> <.button
variant="primary"
navigate={~p"/admin/roles/#{@role}/edit"}
data-testid="role-show-edit-btn"
>
{gettext("Edit role")} {gettext("Edit role")}
</.button> </.button>
<% end %> <% end %>

View file

@ -3116,6 +3116,7 @@ msgid "Edit member"
msgstr "Mitglied bearbeiten" msgstr "Mitglied bearbeiten"
#: lib/mv_web/live/role_live/index.html.heex #: lib/mv_web/live/role_live/index.html.heex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Edit role" msgid "Edit role"
msgstr "Rolle bearbeiten" msgstr "Rolle bearbeiten"
@ -3125,16 +3126,6 @@ msgstr "Rolle bearbeiten"
msgid "Edit user" msgid "Edit user"
msgstr "Benutzer*in bearbeiten" msgstr "Benutzer*in bearbeiten"
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "Rolle bearbeiten"
msgstr "Rolle bearbeiten"
#: 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_field_live/index_component.ex #: lib/mv_web/live/member_field_live/index_component.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Click for datafield details" msgid "Click for datafield details"
@ -3160,11 +3151,26 @@ msgstr "Klicke für Rollen-Details"
msgid "Click for user details" msgid "Click for user details"
msgstr "Klicke für Benutzer*innen-Details" msgstr "Klicke für Benutzer*innen-Details"
#: lib/mv_web/live/custom_field_live/index_component.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Click for dataield details"
msgstr "Klicke für Datenfeld-Details"
#: lib/mv_web/live/member_live/index.html.heex
#, elixir-autogen, elixir-format, fuzzy
msgid "Members table"
msgstr "Mitglieder"
#~ #: lib/mv_web/live/member_field_live/form_component.ex #~ #: lib/mv_web/live/member_field_live/form_component.ex
#~ #, elixir-autogen, elixir-format #~ #, elixir-autogen, elixir-format
#~ msgid "Back to Settings" #~ msgid "Back to Settings"
#~ msgstr "Zurück zu den Einstellungen" #~ msgstr "Zurück zu den Einstellungen"
#~ #: 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/form.ex #~ #: lib/mv_web/live/member_live/form.ex
#~ #, elixir-autogen, elixir-format #~ #, elixir-autogen, elixir-format
#~ msgid "Coming soon" #~ msgid "Coming soon"
@ -3175,6 +3181,11 @@ msgstr "Klicke für Benutzer*innen-Details"
#~ msgid "Reset" #~ msgid "Reset"
#~ msgstr "Zurücksetzen" #~ 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 #~ #: lib/mv_web/live/role_live/form.ex
#~ #, elixir-autogen, elixir-format #~ #, elixir-autogen, elixir-format
#~ msgid "Save Role" #~ msgid "Save Role"

View file

@ -3116,6 +3116,7 @@ msgid "Edit member"
msgstr "" msgstr ""
#: lib/mv_web/live/role_live/index.html.heex #: lib/mv_web/live/role_live/index.html.heex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Edit role" msgid "Edit role"
msgstr "" msgstr ""
@ -3125,16 +3126,6 @@ msgstr ""
msgid "Edit user" msgid "Edit user"
msgstr "" msgstr ""
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "Rolle bearbeiten"
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_field_live/index_component.ex #: lib/mv_web/live/member_field_live/index_component.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Click for datafield details" msgid "Click for datafield details"
@ -3159,3 +3150,13 @@ msgstr ""
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Click for user details" msgid "Click for user details"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/index_component.ex
#, elixir-autogen, elixir-format
msgid "Click for dataield details"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Members table"
msgstr ""

View file

@ -3116,6 +3116,7 @@ msgid "Edit member"
msgstr "" msgstr ""
#: lib/mv_web/live/role_live/index.html.heex #: lib/mv_web/live/role_live/index.html.heex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Edit role" msgid "Edit role"
msgstr "" msgstr ""
@ -3125,16 +3126,6 @@ msgstr ""
msgid "Edit user" msgid "Edit user"
msgstr "" msgstr ""
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "Rolle bearbeiten"
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_field_live/index_component.ex #: lib/mv_web/live/member_field_live/index_component.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Click for datafield details" msgid "Click for datafield details"
@ -3160,11 +3151,26 @@ msgstr ""
msgid "Click for user details" msgid "Click for user details"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/index_component.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Click for dataield details"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex
#, elixir-autogen, elixir-format, fuzzy
msgid "Members table"
msgstr ""
#~ #: lib/mv_web/live/member_field_live/form_component.ex #~ #: lib/mv_web/live/member_field_live/form_component.ex
#~ #, elixir-autogen, elixir-format #~ #, elixir-autogen, elixir-format
#~ msgid "Back to Settings" #~ msgid "Back to Settings"
#~ msgstr "" #~ 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/form.ex #~ #: lib/mv_web/live/member_live/form.ex
#~ #, elixir-autogen, elixir-format #~ #, elixir-autogen, elixir-format
#~ msgid "Coming soon" #~ msgid "Coming soon"
@ -3175,6 +3181,11 @@ msgstr ""
#~ msgid "Reset" #~ msgid "Reset"
#~ msgstr "" #~ 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 #~ #: lib/mv_web/live/role_live/form.ex
#~ #, elixir-autogen, elixir-format, fuzzy #~ #, elixir-autogen, elixir-format, fuzzy
#~ msgid "Save Role" #~ msgid "Save Role"

View file

@ -46,6 +46,35 @@ defmodule MvWeb.MemberLive.IndexTest do
|> Ash.create!(actor: actor) |> Ash.create!(actor: actor)
end end
describe "desktop layout: scroll container and sticky table header" do
@describetag :ui
test "header and filters are outside scroll container; table is in scroll container with lg:max-h and lg:overflow-auto",
%{conn: conn} do
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, ~p"/members")
assert html =~ ~r/data-testid="members-table-scroll"/
# Scroll container has lg: overflow and max-height for desktop-only scroll
assert html =~ "lg:overflow-auto"
assert html =~ "lg:max-h-[calc(100vh-14rem)]"
# Header (page title) is present and not inside the scroll container (scroll container comes after filters)
assert html =~ "Members"
assert html =~ "id=\"members\""
end
test "table thead has sticky classes on desktop when sticky_header is set", %{conn: conn} do
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, ~p"/members")
# CoreComponents table with sticky_header adds lg:sticky lg:top-0 bg-base-100 z-10 to th
assert html =~ "lg:sticky"
assert html =~ "lg:top-0"
assert html =~ "bg-base-100"
end
end
describe "translations" do describe "translations" do
@describetag :ui @describetag :ui