Rework member overview #553
6 changed files with 91 additions and 39 deletions
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -1093,12 +1093,17 @@ defmodule MvWeb.CoreComponents do
|
|||
<tr
|
||||
:for={row <- @rows}
|
||||
id={@row_id && @row_id.(row)}
|
||||
class={[
|
||||
table_row_tr_class(
|
||||
table_row_selected?(assigns, row),
|
||||
@sticky_first_col
|
||||
)
|
||||
]}
|
||||
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,19 +1183,49 @@ 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[: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 -> %>
|
||||
<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))
|
||||
|
||||
if rendered == "" do
|
||||
""
|
||||
if rendered == "" do
|
||||
""
|
||||
else
|
||||
rendered
|
||||
end
|
||||
else
|
||||
rendered
|
||||
end
|
||||
else
|
||||
""
|
||||
end}
|
||||
""
|
||||
end}
|
||||
<% end %>
|
||||
</td>
|
||||
<td :if={@action != []} class="w-0 font-semibold">
|
||||
<div class="flex gap-4">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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", %{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue