diff --git a/assets/css/app.css b/assets/css/app.css
index 7a828ff3..37091909 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -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;
-}
-
diff --git a/assets/js/app.js b/assets/js/app.js
index 91172948..90e36102 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -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() {
diff --git a/lib/mv_web/components/core_components.ex b/lib/mv_web/components/core_components.ex
index 76c44b1c..f8b95f93 100644
--- a/lib/mv_web/components/core_components.ex
+++ b/lib/mv_web/components/core_components.ex
@@ -1093,12 +1093,17 @@ defmodule MvWeb.CoreComponents do
- {if dyn_col[:render] do
- rendered = dyn_col[:render].(@row_item.(row))
+ <%= if dyn_col[:custom_field].value_type == :boolean do %>
+ <% val = dyn_col[:render] && dyn_col[:render].(@row_item.(row)) %>
+ <%= cond do %>
+ <% val == true -> %>
+
+ <.icon
+ name="hero-check-circle"
+ class="size-4 text-success"
+ aria-hidden="true"
+ />
+ {gettext("Yes")}
+
+ <% val == false -> %>
+
+ <.icon
+ name="hero-x-circle"
+ class="size-4 text-error"
+ aria-hidden="true"
+ />
+ {gettext("No")}
+
+ <% true -> %>
+ <.empty_cell sr_text={gettext("Not specified")} />
+ <% end %>
+ <% else %>
+ {if dyn_col[:render] do
+ rendered = dyn_col[:render].(@row_item.(row))
- if rendered == "" do
- ""
+ if rendered == "" do
+ ""
+ else
+ rendered
+ end
else
- rendered
- end
- else
- ""
- end}
+ ""
+ end}
+ <% end %>
diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex
index deea80df..53dab1a4 100644
--- a/lib/mv_web/live/member_live/index.ex
+++ b/lib/mv_web/live/member_live/index.ex
@@ -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
}
diff --git a/test/mv_web/member_live/index_a11y_hardening_test.exs b/test/mv_web/member_live/index_a11y_hardening_test.exs
index 4b6434fc..c40a993c 100644
--- a/test/mv_web/member_live/index_a11y_hardening_test.exs
+++ b/test/mv_web/member_live/index_a11y_hardening_test.exs
@@ -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", %{
diff --git a/test/mv_web/member_live/index_sticky_pinned_test.exs b/test/mv_web/member_live/index_sticky_pinned_test.exs
index 95eac861..d07df8ff 100644
--- a/test/mv_web/member_live/index_sticky_pinned_test.exs
+++ b/test/mv_web/member_live/index_sticky_pinned_test.exs
@@ -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
|