feat(member): stream the overview with keyset infinite scroll instead of loading every member
This commit is contained in:
parent
b09cdf7f3a
commit
b79d7ac9ea
13 changed files with 1067 additions and 451 deletions
|
|
@ -5,6 +5,7 @@
|
|||
<.live_component
|
||||
module={MvWeb.Components.BulkActionsDropdown}
|
||||
id="bulk-actions-dropdown"
|
||||
open={@bulk_actions_open}
|
||||
export_payload_json={@export_payload_json}
|
||||
selected_count={@selected_count}
|
||||
scope={@scope}
|
||||
|
|
@ -39,7 +40,7 @@
|
|||
boolean_filters={@boolean_custom_field_filters}
|
||||
date_custom_fields={@date_custom_fields}
|
||||
date_filters={@date_filters}
|
||||
member_count={length(@members)}
|
||||
member_count={@total_count}
|
||||
/>
|
||||
<.tooltip
|
||||
content={
|
||||
|
|
@ -80,6 +81,19 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<%!-- Polite live region: present on first render (before it is filled) so
|
||||
screen readers announce the exact total matching count on every filter change
|
||||
(WCAG 4.1.3). --%>
|
||||
<div
|
||||
id="members-result-count"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
class="sr-only"
|
||||
>
|
||||
{ngettext("%{count} member", "%{count} members", @total_count, count: @total_count)}
|
||||
</div>
|
||||
|
||||
<%!-- On desktop (lg:), only the table area scrolls; header and filters stay visible. On mobile, normal flow. --%>
|
||||
<div
|
||||
id="members-table-guard"
|
||||
|
|
@ -88,15 +102,21 @@
|
|||
data-testid="members-table-scroll"
|
||||
role="region"
|
||||
aria-label={gettext("Members table")}
|
||||
aria-busy={to_string(@loading?)}
|
||||
>
|
||||
<.table
|
||||
id="members"
|
||||
rows={@members}
|
||||
rows={@streams.members}
|
||||
row_item={fn {_dom_id, member} -> member end}
|
||||
wrapper_overflow_class="overflow-visible"
|
||||
sticky_header={true}
|
||||
sticky_first_col={true}
|
||||
row_id={fn member -> "row-#{member.id}" end}
|
||||
row_click={fn member -> JS.push("select_row_and_navigate", value: %{id: member.id}) end}
|
||||
viewport_bottom={@more? && "load_more"}
|
||||
row_click={
|
||||
fn {_dom_id, member} ->
|
||||
JS.push("select_row_and_navigate", value: %{id: member.id})
|
||||
end
|
||||
}
|
||||
row_tooltip={gettext("Click for member details")}
|
||||
row_selected?={fn member -> MapSet.member?(@selected_members, member.id) end}
|
||||
dynamic_cols={@dynamic_cols}
|
||||
|
|
@ -114,7 +134,7 @@
|
|||
type="checkbox"
|
||||
name="select_all"
|
||||
phx-click="select_all"
|
||||
checked={MapSet.equal?(@selected_members, @members |> Enum.map(& &1.id) |> MapSet.new())}
|
||||
checked={@total_count > 0 and @selected_count == @total_count}
|
||||
aria-label={gettext("Select all members")}
|
||||
role="checkbox"
|
||||
/>
|
||||
|
|
@ -405,6 +425,31 @@
|
|||
</.link>
|
||||
</div>
|
||||
</:action>
|
||||
<:footer>
|
||||
<%!-- Invisible sentinel: starts the next-page load as it approaches the
|
||||
viewport (see the LoadMorePrefetch hook), so rows load ahead of reaching
|
||||
the very bottom. Only rendered while more pages exist (with the footer). --%>
|
||||
<div
|
||||
id="members-load-more-sentinel"
|
||||
phx-hook="LoadMorePrefetch"
|
||||
data-scroll-container="members-table-guard"
|
||||
data-event="load_more"
|
||||
data-margin="600px"
|
||||
aria-hidden="true"
|
||||
class="h-0"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
id="members-loading-bar"
|
||||
phx-hook="StickyViewportWidth"
|
||||
data-scroll-container="members-table-guard"
|
||||
class="sticky left-0 flex items-center justify-center gap-2 border-t border-base-300 bg-base-200 py-4 text-base-content/70"
|
||||
data-testid="members-loading-row"
|
||||
>
|
||||
<span class="loading loading-spinner loading-sm" aria-hidden="true"></span>
|
||||
{gettext("Loading more members …")}
|
||||
</div>
|
||||
</:footer>
|
||||
</.table>
|
||||
</div>
|
||||
</Layouts.app>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue