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