style: highlight selected table and add tooltip
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
carla 2026-02-25 13:16:27 +01:00
parent 02af136fd9
commit 49fd2181a7
19 changed files with 687 additions and 151 deletions

View file

@ -660,6 +660,10 @@ defmodule MvWeb.CoreComponents do
Renders a table with generic styling.
When `row_click` is set, clicking a row (or a data cell) triggers the handler.
Rows with `row_click` get a subtle hover and focus-within outline (theme-friendly ring).
When `selected_row_id` is set and matches a row's id (via `row_value_id` or `row_item.(row).id`),
that row gets a stronger selected outline (ring-primary) for accessibility (not color-only).
The action column has no phx-click on its `<td>`, so action buttons do not trigger row navigation.
For interactive elements inside other columns (e.g. checkboxes, buttons), use
`Phoenix.LiveView.JS.stop_propagation()` in the element's phx-click so the row click is not fired.
@ -670,12 +674,36 @@ defmodule MvWeb.CoreComponents do
<:col :let={user} label="id">{user.id}</:col>
<:col :let={user} label="username">{user.username}</:col>
</.table>
<.table id="members" rows={@members} row_click={fn m -> JS.navigate(~p"/members/#{m}") end} selected_row_id={@selected_member_id}>
<:col :let={m} label="Name">{m.name}</:col>
</.table>
"""
attr :id, :string, required: true
attr :rows, :list, required: true
attr :row_id, :any, default: nil, doc: "the function for generating the row id"
attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
attr :selected_row_id, :any,
default: nil,
doc:
"when set, the row whose id equals this value gets selected styling (single row, e.g. from URL)"
attr :row_selected?, :any,
default: nil,
doc:
"optional; function (row_item) -> boolean to mark multiple rows as selected (e.g. checkbox selection); overrides selected_row_id when set"
attr :row_tooltip, :string,
default: nil,
doc:
"optional; when row_click is set, tooltip text for the row (e.g. gettext(\"Click to view\")). Shown as title on hover and as sr-only for screen readers."
attr :row_value_id, :any,
default: nil,
doc:
"optional; function (row) -> id for comparing with selected_row_id; defaults to row_item.(row).id"
attr :row_item, :any,
default: &Function.identity/1,
doc: "the function for mapping each row before calling the :col and :action slots"
@ -704,6 +732,12 @@ defmodule MvWeb.CoreComponents do
assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
end
# Function to get the row's value id for selected_row_id comparison (no extra DB reads)
row_value_id_fn =
assigns[:row_value_id] || fn row -> assigns.row_item.(row).id end
assigns = assign(assigns, :row_value_id_fn, row_value_id_fn)
~H"""
<div class="overflow-auto">
<table class="table table-zebra">
@ -732,9 +766,15 @@ defmodule MvWeb.CoreComponents do
</tr>
</thead>
<tbody id={@id} phx-update={is_struct(@rows, Phoenix.LiveView.LiveStream) && "stream"}>
<tr :for={row <- @rows} id={@row_id && @row_id.(row)}>
<tr
:for={row <- @rows}
id={@row_id && @row_id.(row)}
class={table_row_tr_class(@row_click, table_row_selected?(assigns, row))}
data-selected={table_row_selected?(assigns, row) && "true"}
title={@row_click && @row_tooltip}
>
<td
:for={col <- @col}
:for={{col, col_idx} <- Enum.with_index(@col)}
phx-click={
(col[:col_click] && col[:col_click].(@row_item.(row))) ||
(@row_click && @row_click.(row))
@ -768,6 +808,9 @@ defmodule MvWeb.CoreComponents do
Enum.join(classes, " ")
}
>
<%= if col_idx == 0 && @row_click && @row_tooltip do %>
<span class="sr-only">{@row_tooltip}</span>
<% end %>
{render_slot(col, @row_item.(row))}
</td>
<td
@ -801,6 +844,43 @@ defmodule MvWeb.CoreComponents do
"""
end
# Returns true if the row is selected (via row_selected?/1 or selected_row_id match).
defp table_row_selected?(assigns, row) do
item = assigns.row_item.(row)
if assigns[:row_selected?] do
assigns.row_selected?.(item)
else
assigns[:selected_row_id] != nil and
assigns.row_value_id_fn.(row) == assigns.selected_row_id
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)
base = []
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"
],
else: base
base = if selected?, do: base ++ ["ring-2", "ring-inset", "ring-primary"], else: base
Enum.join(base, " ")
end
defp table_th_aria_sort(col, sort_field, sort_order) do
col_sort = Map.get(col, :sort_field)