Coverage for src / lexigram / admin / ui / molecules / search_bar.py: 19%
32 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-11 02:25 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-11 02:25 +0800
1"""SearchBar molecule component - combines search input with icon and clear button."""
3from __future__ import annotations
5from typing import Any
7from lexigram.ui import Component, TextInput, Zones, el, get_icon
10class SearchBar(Component):
11 """Reusable search bar with icon and optional clear button."""
13 def __init__(
14 self,
15 name: str = "search",
16 value: str = "",
17 placeholder: str = "Search...",
18 show_icon: bool = True,
19 show_clear: bool = False,
20 **props,
21 ) -> None:
22 """
23 Initialize search bar.
25 Args:
26 name: Input name attribute
27 value: Current search value
28 placeholder: Placeholder text
29 show_icon: Whether to show search icon
30 show_clear: Whether to show clear button
31 **props: Additional props (HTMX attributes, etc.)
32 """
33 super().__init__(
34 name=name,
35 value=value,
36 placeholder=placeholder,
37 show_icon=show_icon,
38 show_clear=show_clear,
39 **props,
40 )
41 self.name = name
42 self.value = value
43 self.placeholder = placeholder
44 self.show_icon = show_icon
45 self.show_clear = show_clear
47 def render(self) -> Any:
48 """Render search bar."""
49 # Filter props to avoid duplicates with explicit args
50 # TextInput args: name, value, placeholder, type, error, disabled, required
51 excluded_props = [
52 "name",
53 "value",
54 "placeholder",
55 "type",
56 "error",
57 "disabled",
58 "required",
59 ]
60 text_input_props = {
61 k: v for k, v in self.props.items() if k not in excluded_props
62 }
64 # AlpineJS State wrapping
65 from lexigram.serialization import dumps_str
67 wrapper_props = {
68 "x_data": f"{{ query: {dumps_str(self.value)} }}",
69 "class_": "relative group",
70 }
72 # Inject x-model into input props
73 text_input_props["x_model"] = "query"
74 # Ensure we trigger updates on input
75 text_input_props["@keydown.escape"] = (
76 "query = ''; $nextTick(() => $el.dispatchEvent(new Event('input', {bubbles: true})))"
77 )
79 search_input = TextInput(
80 name=self.name,
81 value=self.value,
82 placeholder=self.placeholder,
83 autocomplete="off",
84 id=f"{Zones.SEARCH.id}-input",
85 hx_preserve="true",
86 **text_input_props,
87 )
89 # Build Inner Content
90 inner_content = []
92 # Icon
93 if self.show_icon:
94 inner_content.append(
95 el(
96 "div",
97 get_icon("search", size="h-5 w-5"),
98 class_="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-muted-foreground",
99 ),
100 )
102 # Input (wrapped for padding if icon exists)
103 input_html = search_input.render()
104 if self.show_icon:
105 input_html = el(
106 "div",
107 input_html,
108 class_="[&>input]:pl-10 [&>input]:pr-10"
109 if self.show_clear
110 else "[&>input]:pl-10",
111 )
112 elif self.show_clear:
113 input_html = el("div", input_html, class_="[&>input]:pr-10")
115 inner_content.append(input_html)
117 # Clear Button
118 if self.show_clear:
119 clear_btn = el(
120 "button",
121 get_icon("x", size="h-4 w-4", aria_hidden="true"),
122 type="button",
123 aria_label="Clear search",
124 class_="absolute inset-y-0 right-0 pr-3 flex items-center text-muted-foreground hover:text-muted-foreground cursor-pointer",
125 x_show="query.length > 0",
126 # On click: clear query, and dispatch 'input' event on the INPUT element (sibling)
127 # We need to find the input. Since we are inside a relative wrapper,
128 # generic approach: $el.closest('div.relative').querySelector('input').dispatchEvent(...)
129 # Simpler: rely on x-model updating the value, then manually trigger the HTMX on the input.
130 # However, HTMX triggers on the input element.
131 # Use x_on:click
132 **{
133 "@click": "query = ''; $nextTick(() => { let input = $el.closest('.relative').querySelector('input'); input.dispatchEvent(new Event('input', {bubbles: true})); input.dispatchEvent(new Event('change', {bubbles: true})); })",
134 },
135 )
136 inner_content.append(clear_btn)
138 return el("div", *inner_content, **wrapper_props)