From e5a6003ace579d3d4154ae044ffe061bcf6832a9 Mon Sep 17 00:00:00 2001 From: carla Date: Wed, 25 Feb 2026 14:16:43 +0100 Subject: [PATCH] feat: sticky memberstable header --- DESIGN_DUIDELINES.md | 5 + lib/mv_web/components/core_components.ex | 23 +- lib/mv_web/live/member_live/index.html.heex | 589 ++++++++++---------- lib/mv_web/live/role_live/show.ex | 6 +- priv/gettext/de/LC_MESSAGES/default.po | 31 +- priv/gettext/default.pot | 21 +- priv/gettext/en/LC_MESSAGES/default.po | 31 +- test/mv_web/member_live/index_test.exs | 29 + 8 files changed, 411 insertions(+), 324 deletions(-) diff --git a/DESIGN_DUIDELINES.md b/DESIGN_DUIDELINES.md index 98e43db..37428a3 100644 --- a/DESIGN_DUIDELINES.md +++ b/DESIGN_DUIDELINES.md @@ -272,6 +272,11 @@ Notes: - **MUST:** Truncate long values consistently (same max widths for name/email-like fields). - **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-)] 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 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. + --- ## 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 22aeae7..83d506a 100644 --- a/lib/mv_web/components/core_components.ex +++ b/lib/mv_web/components/core_components.ex @@ -715,6 +715,11 @@ defmodule MvWeb.CoreComponents do attr :sort_field, :any, default: nil, doc: "current sort field" 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 attr :label, :string attr :class, :string @@ -745,12 +750,12 @@ defmodule MvWeb.CoreComponents do {col[:label]} - + <.live_component module={MvWeb.Components.SortHeaderComponent} id={:"sort_custom_field_#{dyn_col[:custom_field].id}"} @@ -760,7 +765,7 @@ defmodule MvWeb.CoreComponents do sort_order={@sort_order} /> - + {gettext("Actions")} @@ -891,6 +896,18 @@ defmodule MvWeb.CoreComponents do 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 """ Renders a data list. diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex index eec49de..709e084 100644 --- a/lib/mv_web/live/member_live/index.html.heex +++ b/lib/mv_web/live/member_live/index.html.heex @@ -90,302 +90,311 @@ /> - <.table - id="members" - rows={@members} - 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} + <%!-- On desktop (lg:), only the table area scrolls; header and filters stay visible. On mobile, normal flow. --%> +
- + <.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} - col_click={&MvWeb.MemberLive.Index.checkbox_column_click/1} - label={ - ~H""" + <:col + :let={member} + col_click={&MvWeb.MemberLive.Index.checkbox_column_click/1} + label={ + ~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 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")} + name={member.id} + checked={MapSet.member?(@selected_members, member.id)} + aria-label={gettext("Select member")} role="checkbox" /> - """ - } - > - <.input - type="checkbox" - name={member.id} - checked={MapSet.member?(@selected_members, member.id)} - aria-label={gettext("Select member")} - role="checkbox" - /> - - <:col - :let={member} - :if={:first_name in @member_fields_visible} - label={ - ~H""" - <.live_component - module={MvWeb.Components.SortHeaderComponent} - id={:sort_first_name} - field={:first_name} - label={gettext("First name")} - sort_field={@sort_field} - sort_order={@sort_order} - /> - """ - } - > - {member.first_name} - - <:col - :let={member} - :if={:last_name in @member_fields_visible} - label={ - ~H""" - <.live_component - module={MvWeb.Components.SortHeaderComponent} - id={:sort_last_name} - field={:last_name} - label={gettext("Last name")} - sort_field={@sort_field} - sort_order={@sort_order} - /> - """ - } - > - {member.last_name} - - <:col - :let={member} - :if={:email in @member_fields_visible} - label={ - ~H""" - <.live_component - module={MvWeb.Components.SortHeaderComponent} - id={:sort_email} - field={:email} - label={gettext("Email")} - sort_field={@sort_field} - sort_order={@sort_order} - /> - """ - } - > - {member.email} - - <:col - :let={member} - :if={:join_date in @member_fields_visible} - label={ - ~H""" - <.live_component - module={MvWeb.Components.SortHeaderComponent} - id={:sort_join_date} - field={:join_date} - label={gettext("Join Date")} - sort_field={@sort_field} - sort_order={@sort_order} - /> - """ - } - > - {MvWeb.MemberLive.Index.format_date(member.join_date)} - - <:col - :let={member} - :if={:exit_date in @member_fields_visible} - label={ - ~H""" - <.live_component - module={MvWeb.Components.SortHeaderComponent} - id={:sort_exit_date} - field={:exit_date} - label={gettext("Exit Date")} - sort_field={@sort_field} - sort_order={@sort_order} - /> - """ - } - > - {MvWeb.MemberLive.Index.format_date(member.exit_date)} - - <:col - :let={member} - :if={:notes in @member_fields_visible} - label={gettext("Notes")} - > - {member.notes} - - <:col - :let={member} - :if={:city in @member_fields_visible} - label={ - ~H""" - <.live_component - module={MvWeb.Components.SortHeaderComponent} - id={:sort_city} - field={:city} - label={gettext("City")} - sort_field={@sort_field} - sort_order={@sort_order} - /> - """ - } - > - {member.city} - - <:col - :let={member} - :if={:street in @member_fields_visible} - label={ - ~H""" - <.live_component - module={MvWeb.Components.SortHeaderComponent} - id={:sort_street} - field={:street} - label={gettext("Street")} - sort_field={@sort_field} - sort_order={@sort_order} - /> - """ - } - > - {member.street} - - <:col - :let={member} - :if={:house_number in @member_fields_visible} - label={ - ~H""" - <.live_component - module={MvWeb.Components.SortHeaderComponent} - id={:sort_house_number} - field={:house_number} - label={gettext("House Number")} - sort_field={@sort_field} - sort_order={@sort_order} - /> - """ - } - > - {member.house_number} - - <:col - :let={member} - :if={:postal_code in @member_fields_visible} - label={ - ~H""" - <.live_component - module={MvWeb.Components.SortHeaderComponent} - id={:sort_postal_code} - field={:postal_code} - label={gettext("Postal Code")} - sort_field={@sort_field} - sort_order={@sort_order} - /> - """ - } - > - {member.postal_code} - - <:col - :let={member} - :if={:membership_fee_start_date in @member_fields_visible} - label={ - ~H""" - <.live_component - module={MvWeb.Components.SortHeaderComponent} - id={:sort_membership_fee_start_date} - field={:membership_fee_start_date} - label={gettext("Membership Fee Start Date")} - sort_field={@sort_field} - sort_order={@sort_order} - /> - """ - } - > - {MvWeb.MemberLive.Index.format_date(member.membership_fee_start_date)} - - <:col - :let={member} - :if={:membership_fee_type in @member_fields_visible} - label={ - ~H""" - <.live_component - module={MvWeb.Components.SortHeaderComponent} - id={:sort_membership_fee_type} - field={:membership_fee_type} - label={gettext("Fee Type")} - sort_field={@sort_field} - sort_order={@sort_order} - /> - """ - } - > - <%= if member.membership_fee_type do %> - {member.membership_fee_type.name} - <% else %> - - <% end %> - - <: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( + + <:col + :let={member} + :if={:first_name in @member_fields_visible} + label={ + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_first_name} + field={:first_name} + label={gettext("First name")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ + } + > + {member.first_name} + + <:col + :let={member} + :if={:last_name in @member_fields_visible} + label={ + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_last_name} + field={:last_name} + label={gettext("Last name")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ + } + > + {member.last_name} + + <:col + :let={member} + :if={:email in @member_fields_visible} + label={ + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_email} + field={:email} + label={gettext("Email")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ + } + > + {member.email} + + <:col + :let={member} + :if={:join_date in @member_fields_visible} + label={ + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_join_date} + field={:join_date} + label={gettext("Join Date")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ + } + > + {MvWeb.MemberLive.Index.format_date(member.join_date)} + + <:col + :let={member} + :if={:exit_date in @member_fields_visible} + label={ + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_exit_date} + field={:exit_date} + label={gettext("Exit Date")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ + } + > + {MvWeb.MemberLive.Index.format_date(member.exit_date)} + + <:col + :let={member} + :if={:notes in @member_fields_visible} + label={gettext("Notes")} + > + {member.notes} + + <:col + :let={member} + :if={:city in @member_fields_visible} + label={ + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_city} + field={:city} + label={gettext("City")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ + } + > + {member.city} + + <:col + :let={member} + :if={:street in @member_fields_visible} + label={ + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_street} + field={:street} + label={gettext("Street")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ + } + > + {member.street} + + <:col + :let={member} + :if={:house_number in @member_fields_visible} + label={ + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_house_number} + field={:house_number} + label={gettext("House Number")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ + } + > + {member.house_number} + + <:col + :let={member} + :if={:postal_code in @member_fields_visible} + label={ + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_postal_code} + field={:postal_code} + label={gettext("Postal Code")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ + } + > + {member.postal_code} + + <:col + :let={member} + :if={:membership_fee_start_date in @member_fields_visible} + label={ + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_membership_fee_start_date} + field={:membership_fee_start_date} + label={gettext("Membership Fee Start Date")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ + } + > + {MvWeb.MemberLive.Index.format_date(member.membership_fee_start_date)} + + <:col + :let={member} + :if={:membership_fee_type in @member_fields_visible} + label={ + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_membership_fee_type} + field={:membership_fee_type} + label={gettext("Fee Type")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ + } + > + <%= if member.membership_fee_type do %> + {member.membership_fee_type.name} + <% else %> + + <% end %> + + <: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) ) do %> - - <.icon name={badge.icon} class="size-4" /> - {badge.label} - - <% else %> - {gettext("No cycle")} - <% end %> - - <:col - :let={member} - :if={:groups in @member_fields_visible} - label={ - ~H""" - <.live_component - module={MvWeb.Components.SortHeaderComponent} - id={:sort_groups} - field={:groups} - label={gettext("Groups")} - sort_field={@sort_field} - sort_order={@sort_order} - /> - """ - } - > - <%= for group <- (member.groups || []) do %> - - {group.name} - - <% end %> - <%= if (member.groups || []) == [] do %> - - <% end %> - - <:action :let={member}> -
- <.link navigate={~p"/members/#{member}"} data-testid="member-show-link"> - {gettext("Show")} - -
- - + + <.icon name={badge.icon} class="size-4" /> + {badge.label} + + <% else %> + {gettext("No cycle")} + <% end %> + + <:col + :let={member} + :if={:groups in @member_fields_visible} + label={ + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_groups} + field={:groups} + label={gettext("Groups")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ + } + > + <%= for group <- (member.groups || []) do %> + + {group.name} + + <% end %> + <%= if (member.groups || []) == [] do %> + + <% end %> + + <:action :let={member}> +
+ <.link navigate={~p"/members/#{member}"} data-testid="member-show-link"> + {gettext("Show")} + +
+ + +
diff --git a/lib/mv_web/live/role_live/show.ex b/lib/mv_web/live/role_live/show.ex index dd2c4f2..b5a25ce 100644 --- a/lib/mv_web/live/role_live/show.ex +++ b/lib/mv_web/live/role_live/show.ex @@ -174,7 +174,11 @@ defmodule MvWeb.RoleLive.Show do {gettext("Back")} <%= 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")} <% end %> diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 4561f24..4e6c888 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -3116,6 +3116,7 @@ msgid "Edit member" msgstr "Mitglied bearbeiten" #: lib/mv_web/live/role_live/index.html.heex +#: lib/mv_web/live/role_live/show.ex #, elixir-autogen, elixir-format, fuzzy msgid "Edit role" msgstr "Rolle bearbeiten" @@ -3125,16 +3126,6 @@ msgstr "Rolle bearbeiten" msgid "Edit user" 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 #, elixir-autogen, elixir-format msgid "Click for datafield details" @@ -3160,11 +3151,26 @@ msgstr "Klicke für Rollen-Details" msgid "Click for user 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 #~ #, 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 +#~ msgid "Click for custom field details" +#~ msgstr "Klicke für Datenfeld-Details" + #~ #: lib/mv_web/live/member_live/form.ex #~ #, elixir-autogen, elixir-format #~ msgid "Coming soon" @@ -3175,6 +3181,11 @@ msgstr "Klicke für Benutzer*innen-Details" #~ 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" diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index cea7991..ed020a0 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -3116,6 +3116,7 @@ msgid "Edit member" msgstr "" #: lib/mv_web/live/role_live/index.html.heex +#: lib/mv_web/live/role_live/show.ex #, elixir-autogen, elixir-format msgid "Edit role" msgstr "" @@ -3125,16 +3126,6 @@ msgstr "" msgid "Edit user" 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 #, elixir-autogen, elixir-format msgid "Click for datafield details" @@ -3159,3 +3150,13 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Click for user details" 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 "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 9f38efe..a44e87c 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -3116,6 +3116,7 @@ msgid "Edit member" msgstr "" #: lib/mv_web/live/role_live/index.html.heex +#: lib/mv_web/live/role_live/show.ex #, elixir-autogen, elixir-format, fuzzy msgid "Edit role" msgstr "" @@ -3125,16 +3126,6 @@ msgstr "" msgid "Edit user" 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 #, elixir-autogen, elixir-format msgid "Click for datafield details" @@ -3160,11 +3151,26 @@ msgstr "" msgid "Click for user details" 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 #~ #, elixir-autogen, elixir-format #~ msgid "Back to Settings" #~ 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 #~ #, elixir-autogen, elixir-format #~ msgid "Coming soon" @@ -3175,6 +3181,11 @@ msgstr "" #~ 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" diff --git a/test/mv_web/member_live/index_test.exs b/test/mv_web/member_live/index_test.exs index 1c8328f..b75fcd8 100644 --- a/test/mv_web/member_live/index_test.exs +++ b/test/mv_web/member_live/index_test.exs @@ -46,6 +46,35 @@ defmodule MvWeb.MemberLive.IndexTest do |> Ash.create!(actor: actor) 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 @describetag :ui