feat(member): bring the overview table up to WCAG 2.2 AA

This commit is contained in:
Simon 2026-07-06 10:49:10 +02:00
parent 0c375234c8
commit c2cb3edab8
6 changed files with 91 additions and 39 deletions

View file

@ -957,16 +957,3 @@
margin-bottom: 0;
}
/*
* Sort-header tooltips must render on top of the pinned checkbox column's
* horizontal-scroll fade (::after on .sticky-first-col-th). The hovered/focused
* header th is already lifted above the checkbox th (hover/focus-within:z-50),
* so the whole tooltip wins the stacking; this rule additionally keeps the
* daisyUI tooltip bubble's own pseudo-elements above the fade overlay. Scoped
* to the member overview header so no unrelated tooltip is affected.
*/
#members-keyboard thead .tooltip::before,
#members-keyboard thead .tooltip::after {
z-index: 60;
}

View file

@ -36,6 +36,22 @@ function getBrowserTimezone() {
// Hooks for LiveView components
let Hooks = {}
// IndeterminateCheckbox: the `indeterminate` state of a checkbox is a JS
// property, not an HTML attribute, so it cannot be set from the server render.
// Mirror it from the data-indeterminate attribute on mount and every update
// (e.g. select-all reflecting a partial selection — WCAG select-all semantics).
Hooks.IndeterminateCheckbox = {
updateIndeterminate() {
this.el.indeterminate = this.el.dataset.indeterminate === "true"
},
mounted() {
this.updateIndeterminate()
},
updated() {
this.updateIndeterminate()
}
}
// CopyToClipboard hook: Copies text to clipboard when triggered by server event
Hooks.CopyToClipboard = {
mounted() {

View file

@ -1093,12 +1093,17 @@ defmodule MvWeb.CoreComponents do
<tr
:for={row <- @rows}
id={@row_id && @row_id.(row)}
class={[
class={
[
table_row_tr_class(
table_row_selected?(assigns, row),
@sticky_first_col
)
]}
),
# WCAG 2.4.11: keep a keyboard-focused row from being hidden under
# the sticky header by reserving header-height scroll margin.
@sticky_header && "scroll-mt-16"
]
}
data-row-interactive={@row_click && "true"}
data-selected={table_row_selected?(assigns, row) && "true"}
title={@row_click && @row_tooltip}
@ -1178,8 +1183,37 @@ defmodule MvWeb.CoreComponents do
<td
:for={dyn_col <- @dynamic_cols}
phx-click={@row_click && @row_click.(row)}
class={["max-w-xs truncate", @row_click && "hover:cursor-pointer"]}
class={[
"max-w-xs",
dyn_col[:custom_field].value_type != :boolean && "truncate",
@row_click && "hover:cursor-pointer"
]}
>
<%= if dyn_col[:custom_field].value_type == :boolean do %>
<% val = dyn_col[:render] && dyn_col[:render].(@row_item.(row)) %>
<%= cond do %>
<% val == true -> %>
<div class="flex justify-center">
<.icon
name="hero-check-circle"
class="size-4 text-success"
aria-hidden="true"
/>
<span class="sr-only">{gettext("Yes")}</span>
</div>
<% val == false -> %>
<div class="flex justify-center">
<.icon
name="hero-x-circle"
class="size-4 text-error"
aria-hidden="true"
/>
<span class="sr-only">{gettext("No")}</span>
</div>
<% true -> %>
<.empty_cell sr_text={gettext("Not specified")} />
<% end %>
<% else %>
{if dyn_col[:render] do
rendered = dyn_col[:render].(@row_item.(row))
@ -1191,6 +1225,7 @@ defmodule MvWeb.CoreComponents do
else
""
end}
<% end %>
</td>
<td :if={@action != []} class="w-0 font-semibold">
<div class="flex gap-4">

View file

@ -767,8 +767,15 @@ defmodule MvWeb.MemberLive.Index do
custom_field: custom_field,
render: fn member ->
case get_custom_field_value(member, custom_field) do
nil -> ""
cfv -> Formatter.format_custom_field_value(cfv.value, custom_field)
nil ->
""
cfv ->
if custom_field.value_type == :boolean do
extract_boolean_value(cfv.value)
else
Formatter.format_custom_field_value(cfv.value, custom_field)
end
end
end
}

View file

@ -47,13 +47,16 @@ defmodule MvWeb.MemberLive.IndexA11yHardeningTest do
test "interactive row targets meet the >=24px minimum in both densities", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/members")
# Compact (default) and comfortable both keep the >=24px checkbox target.
assert has_element?(view, "input[type='checkbox'].min-h-6.min-w-6")
# The row/select-all checkboxes are pinned to 1.5rem (24px) in both densities
# via the `#members-table-guard input.checkbox` CSS rule (WCAG 2.5.8), which
# overrides DaisyUI's density-proportional sizing. The faithful proxy in a
# LiveView test is that the guarded checkbox markup exists in both densities.
assert has_element?(view, "#members-table-guard input[type='checkbox'].checkbox")
# Switch to comfortable via the view-settings dropdown.
view |> element("[data-testid='view-settings-button']") |> render_click()
view |> element("[data-testid='view-setting-density']") |> render_click()
assert has_element?(view, "input[type='checkbox'].min-h-6.min-w-6")
assert has_element?(view, "#members-table-guard input[type='checkbox'].checkbox")
end
test "select-all reflects indeterminate for a partial selection", %{

View file

@ -1,8 +1,10 @@
defmodule MvWeb.MemberLive.IndexStickyPinnedTest do
@moduledoc """
§1.19 When the table scrolls vertically and horizontally, the header stays
sticky and the identifier (Name) column stays pinned; overflowing cell content
truncates with ellipsis and exposes a hover/focus tooltip.
sticky. Horizontal pinning is limited to the checkbox column (with a scroll
fade marking the boundary) rather than also pinning the Name column;
overflowing cell content truncates with ellipsis and exposes a hover/focus
tooltip.
"""
use MvWeb.ConnCase, async: false
@ -30,16 +32,18 @@ defmodule MvWeb.MemberLive.IndexStickyPinnedTest do
%{conn: conn_with_oidc_user(conn)}
end
test "header is sticky and the identifier column is pinned", %{conn: conn} do
test "header is sticky and only the checkbox column is pinned", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/members")
# Sticky header (desktop).
assert html =~ "lg:sticky"
assert html =~ "lg:top-0"
# Checkbox column pinned at left-0, identifier (Name) column pinned beside it.
# Only the checkbox column is pinned (left-0) now; a scroll fade marks the
# boundary instead of also pinning the Name column at left-12.
assert html =~ "left-0"
assert html =~ "left-12"
assert html =~ "sticky-first-col-th"
refute html =~ "left-12"
end
test "composite cells truncate and expose a tooltip via title", %{conn: conn} do