Coverage for src / lexigram / ui / molecules / virtual_scroll.py: 31%
32 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-10 04:11 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-10 04:11 +0800
1"""Virtual Scroll component for Lexigram Admin."""
3from __future__ import annotations
5from typing import Any
7from lexigram.ui.core.base import Component, el
10class VirtualScroll(Component):
11 """Component for handling infinite scroll and virtualization.
13 This uses HTMX 'revealed' trigger to load next chunks of data.
14 """
16 def __init__(
17 self,
18 url: str,
19 total_items: int | None = None,
20 chunk_size: int = 50,
21 target_id: str | None = None,
22 placeholder: Any | None = None,
23 **props,
24 ):
25 super().__init__(**props)
26 self.url = url
27 self.total_items = total_items
28 self.chunk_size = chunk_size
29 self.target_id = target_id
30 self.placeholder = placeholder
32 def render(self) -> Any:
33 return el(
34 "div",
35 self.children,
36 id=self.target_id or "virtual-scroll-container",
37 class_="virtual-scroll",
38 **self.props,
39 )
42def render_infinite_row(
43 row_content: Any,
44 next_url: str | None = None,
45 trigger: str = "revealed",
46 threshold: str = "0.5",
47) -> Any:
48 """Helper to render a row that triggers the next page load.
50 Args:
51 row_content: The content of the current row
52 next_url: URL to fetch the next page. If None, no trigger is added.
53 trigger: HTMX trigger (default 'revealed')
54 threshold: Intersection observer threshold
55 """
56 if not next_url:
57 return row_content
59 attrs = {
60 "hx-get": next_url,
61 "hx-trigger": f"{trigger} threshold:{threshold}",
62 "hx-swap": "afterend",
63 }
65 if hasattr(row_content, "attrs"):
66 row_content.attrs.update(attrs)
67 return row_content
69 return el("div", row_content, **attrs)
72class InfiniteScrollTrigger(Component):
73 """A component that triggers an HTMX request when scrolled into view."""
75 def __init__(
76 self,
77 url: str,
78 trigger: str = "revealed",
79 target: str | None = None,
80 swap: str = "afterend",
81 select: str | None = None,
82 **props,
83 ):
84 super().__init__(**props)
85 self.url = url
86 self.trigger = trigger
87 self.target = target
88 self.swap = swap
89 self.select = select or "#table-content > *"
91 def render(self) -> Any:
92 children = self.children or [
93 el(
94 "div",
95 el(
96 "i",
97 class_="fas fa-spinner fa-spin mr-2 text-primary",
98 ),
99 el("span", "Loading more...", class_="text-muted-foreground text-sm"),
100 class_="flex items-center justify-center p-4 w-full",
101 ),
102 ]
104 return el(
105 "div",
106 *children,
107 **{
108 "hx-get": self.url,
109 "hx-trigger": self.trigger,
110 "hx-target": self.target,
111 "hx-swap": self.swap,
112 "hx-select": self.select,
113 **self.props,
114 },
115 )