Coverage for src / lexigram / ui / atoms / inputs / selection / select.py: 26%
50 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
1from __future__ import annotations
3from typing import Any
5from lexigram.ui.atoms.inputs.base import AbstractInput
6from lexigram.ui.core.base import el
9class Select(AbstractInput):
10 """Select dropdown with label and error support.
12 Args:
13 name: Input name attribute
14 choices: List of (value, label) tuples
15 value: Currently selected value
16 label: Optional label text
17 error: Error message to display
18 multiple: Whether multiple selection is allowed
19 disabled: Whether input is disabled
20 required: Whether input is required
21 """
23 def __init__(
24 self,
25 name: str,
26 choices: list[tuple[str, str]] | None = None,
27 multiple: bool = False,
28 **kwargs,
29 ) -> None:
30 super().__init__(name=name, **kwargs)
31 self.choices = choices or []
32 self.multiple = multiple
34 def _render_input(self) -> Any:
35 options = []
36 for val, label in self.choices:
37 is_selected = False
38 if self.multiple and isinstance(self.value, (list, tuple)):
39 is_selected = val in self.value
40 else:
41 is_selected = str(val) == str(
42 self.value if self.value is not None else "",
43 )
45 options.append(
46 el(
47 "option",
48 label,
49 value=val,
50 selected=is_selected,
51 ),
52 )
54 # Merge HTMX triggers for cascading dropdowns
55 extra_props = self._get_extra_props(exclude=["choices", "multiple"])
56 depends_on = self.props.get("depends_on")
57 options_from = self.props.get("options_from")
58 if depends_on and options_from:
59 extra_props["hx-get"] = options_from
60 extra_props["hx-trigger"] = f"change from:#{depends_on}"
61 extra_props["hx-target"] = "this"
62 extra_props["hx-swap"] = "innerHTML"
63 extra_props["hx-include"] = f"#{depends_on}"
65 return el(
66 "select",
67 *options,
68 name=self.name,
69 id=self.input_id,
70 multiple=self.multiple,
71 disabled=self.disabled,
72 required=self.required,
73 class_=self._get_input_classes(
74 "pl-3 pr-10 h-8 shadow-sm ring-1 ring-inset sm:text-sm leading-5",
75 ),
76 **extra_props,
77 )
80class NativeMultiSelect(Select):
81 """Native browser multiple select dropdown."""
83 def __init__(self, *args, **kwargs) -> None:
84 kwargs["multiple"] = True
85 super().__init__(*args, **kwargs)
88class LazySelect(Select):
89 """Select dropdown that support infinite scroll / lazy loading of options."""
91 def __init__(
92 self,
93 name: str,
94 lazy_url: str,
95 choices: list[tuple[str, str]] | None = None,
96 page: int = 1,
97 **kwargs,
98 ) -> None:
99 super().__init__(name, choices=choices, **kwargs)
100 self.lazy_url = lazy_url
101 self.page = page
103 def render_options(self) -> list[Any]:
104 options = []
105 for val, label in self.choices:
106 is_selected = str(val) == str(self.value if self.value is not None else "")
107 options.append(el("option", label, value=val, selected=is_selected))
109 next_page = self.page + 1
110 sentinel = el(
111 "option",
112 "Loading more...",
113 value="",
114 disabled=True,
115 **{
116 "hx-get": f"{self.lazy_url}?page={next_page}",
117 "hx-trigger": "intersect once",
118 "hx-swap": "afterend",
119 "hx-indicator": "this",
120 },
121 )
122 options.append(sentinel)
123 return options
125 def _render_input(self) -> Any:
126 if self.props.get("is_fragment"):
127 return self.render_options()
129 options = self.render_options()
131 return el(
132 "select",
133 *options,
134 name=self.name,
135 id=self.input_id,
136 multiple=self.multiple,
137 disabled=self.disabled,
138 required=self.required,
139 class_=self._get_input_classes(
140 "pl-3 pr-10 h-8 shadow-sm ring-1 ring-inset sm:text-sm leading-5",
141 ),
142 **self._get_extra_props(exclude=["lazy_url", "page", "is_fragment"]),
143 )