feat: make checkbox column in member view sticky
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing

This commit is contained in:
Simon 2026-05-08 11:37:04 +02:00
parent f3043df58b
commit 93e1ec7414
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
7 changed files with 234 additions and 35 deletions

View file

@ -943,6 +943,11 @@ defmodule MvWeb.CoreComponents do
doc:
"overflow class for the table wrapper; set to overflow-visible when outer container owns scrolling"
attr :sticky_first_col, :boolean,
default: false,
doc:
"when true, first header/body column gets sticky left positioning to keep selection controls visible"
slot :col, required: true do
attr :label, :string
attr :class, :string
@ -980,14 +985,18 @@ defmodule MvWeb.CoreComponents do
<div
id={@row_click && "#{@id}-keyboard"}
class={@wrapper_overflow_class}
data-sticky-first-col-rows={@sticky_first_col && "true"}
phx-hook={@row_click && "TableRowKeydown"}
>
<table class="table table-zebra">
<thead>
<tr>
<th
:for={col <- @col}
class={table_th_class(col, @sticky_header)}
:for={{col, col_idx} <- Enum.with_index(@col)}
class={[
table_th_class(col, @sticky_header),
@sticky_first_col && col_idx == 0 && "sticky left-0 z-30 bg-base-100"
]}
aria-sort={table_th_aria_sort(col, @sort_field, @sort_order)}
>
{col[:label]}
@ -1011,7 +1020,14 @@ defmodule MvWeb.CoreComponents do
<tr
:for={row <- @rows}
id={@row_id && @row_id.(row)}
class={table_row_tr_class(@row_click, table_row_selected?(assigns, row))}
class={[
table_row_tr_class(
@row_click,
table_row_selected?(assigns, row),
@sticky_first_col
)
]}
data-row-interactive={@row_click && "true"}
data-selected={table_row_selected?(assigns, row) && "true"}
title={@row_click && @row_tooltip}
>
@ -1031,6 +1047,13 @@ defmodule MvWeb.CoreComponents do
has_click = col[:col_click] || @row_click
classes = ["max-w-xs"]
classes =
if @sticky_first_col && col_idx == 0 do
["sticky-first-col-cell sticky left-0 z-20" | classes]
else
classes
end
classes =
if col_class == nil || (col_class && !String.contains?(col_class, "text-center")) do
["truncate" | classes]
@ -1045,7 +1068,7 @@ defmodule MvWeb.CoreComponents do
classes
end
# WCAG: no focus ring on the cell itself; row shows focus via focus-within
# WCAG: no focus ring on the cell itself; sticky zebra rows show keyboard focus via CSS :has(:focus-visible)
classes =
if @row_click && @first_row_click_col_idx == col_idx do
[
@ -1116,28 +1139,18 @@ defmodule MvWeb.CoreComponents do
end
end
# Returns CSS classes for table row: hover/focus-within outline when row_click is set,
# and stronger selected outline when selected (WCAG: not color-only).
# Hover/focus-within are omitted for the selected row so the selected ring stays visible.
defp table_row_tr_class(row_click, selected?) do
has_row_click? = not is_nil(row_click)
# Returns CSS classes for table row selection styles.
# Hover/focus row highlighting is CSS-driven via [data-row-interactive] selectors in app.css.
# Sticky-first-column zebra tables use CSS accents and omit selected row ring classes.
defp table_row_tr_class(_row_click, selected?, sticky_first_col) do
base = []
# Sticky-first-col tables: selection/hover accents are CSS-only (orange bar + fills).
base =
if has_row_click? and not selected?,
do:
base ++
[
"hover:ring-2",
"hover:ring-inset",
"hover:ring-base-content/10",
"focus-within:ring-2",
"focus-within:ring-inset",
"focus-within:ring-base-content/10"
],
if selected? and not sticky_first_col,
do: base ++ ["ring-2", "ring-inset", "ring-primary"],
else: base
base = if selected?, do: base ++ ["ring-2", "ring-inset", "ring-primary"], else: base
Enum.join(base, " ")
end

View file

@ -107,6 +107,7 @@
rows={@members}
wrapper_overflow_class="overflow-visible"
sticky_header={true}
sticky_first_col={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")}