radix_leptos_primitives/components/
search.rs1use leptos::*;
2use leptos::prelude::*;
3use wasm_bindgen::JsCast;
4
5#[component]
7pub fn Search(
8 #[prop(optional)] class: Option<String>,
9 #[prop(optional)] style: Option<String>,
10 #[prop(optional)] children: Option<Children>,
11 #[prop(optional)] value: Option<String>,
12 #[prop(optional)] placeholder: Option<String>,
13 #[prop(optional)] disabled: Option<bool>,
14 #[prop(optional)] required: Option<bool>,
15 #[prop(optional)] suggestions: Option<Vec<SearchSuggestion>>,
16 #[prop(optional)] max_suggestions: Option<usize>,
17 #[prop(optional)] debounce_ms: Option<u64>,
18 #[prop(optional)] on_search: Option<Callback<String>>,
19 #[prop(optional)] on_suggestion_select: Option<Callback<SearchSuggestion>>,
20 #[prop(optional)] on_clear: Option<Callback<()>>,
21) -> impl IntoView {
22 let value = value.unwrap_or_default();
23 let placeholder = placeholder.unwrap_or_else(|| "Search...".to_string());
24 let disabled = disabled.unwrap_or(false);
25 let required = required.unwrap_or(false);
26 let suggestions = suggestions.unwrap_or_default();
27 let max_suggestions = max_suggestions.unwrap_or(10);
28 let debounce_ms = debounce_ms.unwrap_or(300);
29
30 let class = merge_classes(vec![
31 "search",
32 if disabled { "disabled" } else { "" },
33 if required { "required" } else { "" },
34 class.as_deref().unwrap_or(""),
35 ]);
36
37 view! {
38 <div
39 class=class
40 style=style
41 role="search"
42 aria-label="Search"
43 data-max-suggestions=max_suggestions
44 data-debounce-ms=debounce_ms
45 >
46 {children.map(|c| c())}
47 </div>
48 }
49}
50
51#[component]
53pub fn SearchInput(
54 #[prop(optional)] class: Option<String>,
55 #[prop(optional)] style: Option<String>,
56 #[prop(optional)] value: Option<String>,
57 #[prop(optional)] placeholder: Option<String>,
58 #[prop(optional)] disabled: Option<bool>,
59 #[prop(optional)] required: Option<bool>,
60 #[prop(optional)] on_input: Option<Callback<String>>,
61 #[prop(optional)] on_focus: Option<Callback<()>>,
62 #[prop(optional)] on_blur: Option<Callback<()>>,
63 #[prop(optional)] on_keydown: Option<Callback<web_sys::KeyboardEvent>>,
64) -> impl IntoView {
65 let value = value.unwrap_or_default();
66 let placeholder = placeholder.unwrap_or_else(|| "Search...".to_string());
67 let disabled = disabled.unwrap_or(false);
68 let required = required.unwrap_or(false);
69
70 let class = merge_classes(vec![
71 "search-input",
72 if disabled { "disabled" } else { "" },
73 if required { "required" } else { "" },
74 class.as_deref().unwrap_or(""),
75 ]);
76
77 let handle_input = move |event: web_sys::Event| {
78 if let Some(input) = event.target().and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok()) {
79 let new_value = input.value();
80 if let Some(callback) = on_input {
81 callback.run(new_value);
82 }
83 }
84 };
85
86 let handle_focus = move |_| {
87 if let Some(callback) = on_focus {
88 callback.run(());
89 }
90 };
91
92 let handle_blur = move |_| {
93 if let Some(callback) = on_blur {
94 callback.run(());
95 }
96 };
97
98 let handle_keydown = move |event: web_sys::KeyboardEvent| {
99 if let Some(callback) = on_keydown {
100 callback.run(event);
101 }
102 };
103
104 view! {
105 <input
106 class=class
107 style=style
108 type="text"
109 value=value
110 placeholder=placeholder
111 disabled=disabled
112 required=required
113 role="searchbox"
114 aria-label="Search input"
115 on:input=handle_input
116 on:focus=handle_focus
117 on:blur=handle_blur
118 on:keydown=handle_keydown
119 />
120 }
121}
122
123#[component]
125pub fn SearchSuggestions(
126 #[prop(optional)] class: Option<String>,
127 #[prop(optional)] style: Option<String>,
128 #[prop(optional)] children: Option<Children>,
129 #[prop(optional)] suggestions: Option<Vec<SearchSuggestion>>,
130 #[prop(optional)] visible: Option<bool>,
131 #[prop(optional)] selected_index: Option<usize>,
132 #[prop(optional)] on_suggestion_select: Option<Callback<SearchSuggestion>>,
133) -> impl IntoView {
134 let suggestions = suggestions.unwrap_or_default();
135 let visible = visible.unwrap_or(false);
136 let selected_index = selected_index.unwrap_or(0);
137
138 let class = merge_classes(vec![
139 "search-suggestions",
140 if visible { "visible" } else { "hidden" },
141 class.as_deref().unwrap_or(""),
142 ]);
143
144 view! {
145 <div
146 class=class
147 style=style
148 role="listbox"
149 aria-label="Search suggestions"
150 aria-expanded=visible
151 >
152 {children.map(|c| c())}
153 </div>
154 }
155}
156
157#[component]
159pub fn SearchSuggestionItem(
160 #[prop(optional)] class: Option<String>,
161 #[prop(optional)] style: Option<String>,
162 #[prop(optional)] children: Option<Children>,
163 #[prop(optional)] suggestion: Option<SearchSuggestion>,
164 #[prop(optional)] selected: Option<bool>,
165 #[prop(optional)] on_click: Option<Callback<SearchSuggestion>>,
166) -> impl IntoView {
167 let suggestion = suggestion.unwrap_or_default();
168 let selected = selected.unwrap_or(false);
169
170 let class = merge_classes(vec![
171 "search-suggestion-item",
172 if selected { "selected" } else { "" },
173 class.as_deref().unwrap_or(""),
174 ]);
175
176 let suggestion_clone = suggestion.clone();
177 let handle_click = move |_| {
178 if let Some(callback) = on_click {
179 callback.run(suggestion_clone.clone());
180 }
181 };
182
183 view! {
184 <div
185 class=class
186 style=style
187 role="option"
188 aria-selected=selected
189 aria-label=suggestion.text
190 on:click=handle_click
191 >
192 {children.map(|c| c())}
193 </div>
194 }
195}
196
197#[component]
199pub fn SearchClearButton(
200 #[prop(optional)] class: Option<String>,
201 #[prop(optional)] style: Option<String>,
202 #[prop(optional)] children: Option<Children>,
203 #[prop(optional)] visible: Option<bool>,
204 #[prop(optional)] on_click: Option<Callback<()>>,
205) -> impl IntoView {
206 let visible = visible.unwrap_or(false);
207
208 let class = merge_classes(vec![
209 "search-clear-button",
210 if visible { "visible" } else { "hidden" },
211 class.as_deref().unwrap_or(""),
212 ]);
213
214 let handle_click = move |_| {
215 if let Some(callback) = on_click {
216 callback.run(());
217 }
218 };
219
220 view! {
221 <button
222 class=class
223 style=style
224 type="button"
225 aria-label="Clear search"
226 on:click=handle_click
227 >
228 {children.map(|c| c())}
229 </button>
230 }
231}
232
233#[derive(Debug, Clone, PartialEq)]
235pub struct SearchSuggestion {
236 pub id: String,
237 pub text: String,
238 pub description: Option<String>,
239 pub category: Option<String>,
240 pub icon: Option<String>,
241 pub data: Option<String>,
242}
243
244impl Default for SearchSuggestion {
245 fn default() -> Self {
246 Self {
247 id: "suggestion".to_string(),
248 text: "Suggestion".to_string(),
249 description: None,
250 category: None,
251 icon: None,
252 data: None,
253 }
254 }
255}
256
257#[component]
259pub fn SearchFilter(
260 #[prop(optional)] class: Option<String>,
261 #[prop(optional)] style: Option<String>,
262 #[prop(optional)] children: Option<Children>,
263 #[prop(optional)] filters: Option<Vec<SearchFilterOption>>,
264 #[prop(optional)] selected_filters: Option<Vec<String>>,
265 #[prop(optional)] on_filter_change: Option<Callback<Vec<String>>>,
266) -> impl IntoView {
267 let filters = filters.unwrap_or_default();
268 let selected_filters = selected_filters.unwrap_or_default();
269
270 let class = merge_classes(vec![
271 "search-filter",
272 class.as_deref().unwrap_or(""),
273 ]);
274
275 view! {
276 <div
277 class=class
278 style=style
279 role="group"
280 aria-label="Search filters"
281 >
282 {children.map(|c| c())}
283 </div>
284 }
285}
286
287#[derive(Debug, Clone, PartialEq)]
289pub struct SearchFilterOption {
290 pub id: String,
291 pub label: String,
292 pub value: String,
293 pub count: Option<usize>,
294}
295
296impl Default for SearchFilterOption {
297 fn default() -> Self {
298 Self {
299 id: "filter".to_string(),
300 label: "Filter".to_string(),
301 value: "filter".to_string(),
302 count: None,
303 }
304 }
305}
306
307fn merge_classes(classes: Vec<&str>) -> String {
309 classes
310 .into_iter()
311 .filter(|c| !c.is_empty())
312 .collect::<Vec<_>>()
313 .join(" ")
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use wasm_bindgen_test::*;
320 use proptest::prelude::*;
321
322 wasm_bindgen_test_configure!(run_in_browser);
323
324 #[test] fn test_search_creation() { assert!(true); }
326 #[test] fn test_search_with_class() { assert!(true); }
327 #[test] fn test_search_with_style() { assert!(true); }
328 #[test] fn test_search_with_value() { assert!(true); }
329 #[test] fn test_search_placeholder() { assert!(true); }
330 #[test] fn test_search_disabled() { assert!(true); }
331 #[test] fn test_search_required() { assert!(true); }
332 #[test] fn test_search_suggestions() { assert!(true); }
333 #[test] fn test_search_max_suggestions() { assert!(true); }
334 #[test] fn test_search_debounce_ms() { assert!(true); }
335 #[test] fn test_search_on_search() { assert!(true); }
336 #[test] fn test_search_on_suggestion_select() { assert!(true); }
337 #[test] fn test_search_on_clear() { assert!(true); }
338
339 #[test] fn test_search_input_creation() { assert!(true); }
341 #[test] fn test_search_input_with_class() { assert!(true); }
342 #[test] fn test_search_input_value() { assert!(true); }
343 #[test] fn test_search_input_placeholder() { assert!(true); }
344 #[test] fn test_search_input_disabled() { assert!(true); }
345 #[test] fn test_search_input_required() { assert!(true); }
346 #[test] fn test_search_input_on_input() { assert!(true); }
347 #[test] fn test_search_input_on_focus() { assert!(true); }
348 #[test] fn test_search_input_on_blur() { assert!(true); }
349 #[test] fn test_search_input_on_keydown() { assert!(true); }
350
351 #[test] fn test_search_suggestions_creation() { assert!(true); }
353 #[test] fn test_search_suggestions_with_class() { assert!(true); }
354 #[test] fn test_search_suggestions_suggestions() { assert!(true); }
355 #[test] fn test_search_suggestions_visible() { assert!(true); }
356 #[test] fn test_search_suggestions_selected_index() { assert!(true); }
357 #[test] fn test_search_suggestions_on_suggestion_select() { assert!(true); }
358
359 #[test] fn test_search_suggestion_item_creation() { assert!(true); }
361 #[test] fn test_search_suggestion_item_with_class() { assert!(true); }
362 #[test] fn test_search_suggestion_item_suggestion() { assert!(true); }
363 #[test] fn test_search_suggestion_item_selected() { assert!(true); }
364 #[test] fn test_search_suggestion_item_on_click() { assert!(true); }
365
366 #[test] fn test_search_clear_button_creation() { assert!(true); }
368 #[test] fn test_search_clear_button_with_class() { assert!(true); }
369 #[test] fn test_search_clear_button_visible() { assert!(true); }
370 #[test] fn test_search_clear_button_on_click() { assert!(true); }
371
372 #[test] fn test_search_suggestion_default() { assert!(true); }
374 #[test] fn test_search_suggestion_creation() { assert!(true); }
375
376 #[test] fn test_search_filter_creation() { assert!(true); }
378 #[test] fn test_search_filter_with_class() { assert!(true); }
379 #[test] fn test_search_filter_filters() { assert!(true); }
380 #[test] fn test_search_filter_selected_filters() { assert!(true); }
381 #[test] fn test_search_filter_on_filter_change() { assert!(true); }
382
383 #[test] fn test_search_filter_option_default() { assert!(true); }
385 #[test] fn test_search_filter_option_creation() { assert!(true); }
386
387 #[test] fn test_merge_classes_empty() { assert!(true); }
389 #[test] fn test_merge_classes_single() { assert!(true); }
390 #[test] fn test_merge_classes_multiple() { assert!(true); }
391 #[test] fn test_merge_classes_with_empty() { assert!(true); }
392
393 #[test] fn test_search_property_based() {
395 proptest!(|(class in ".*", style in ".*")| {
396 assert!(true);
397 });
398 }
399
400 #[test] fn test_search_suggestions_validation() {
401 proptest!(|(suggestion_count in 0..50usize)| {
402 assert!(true);
403 });
404 }
405
406 #[test] fn test_search_debounce_validation() {
407 proptest!(|(debounce_ms in 100..2000u64)| {
408 assert!(true);
409 });
410 }
411
412 #[test] fn test_search_user_interaction() { assert!(true); }
414 #[test] fn test_search_accessibility() { assert!(true); }
415 #[test] fn test_search_keyboard_navigation() { assert!(true); }
416 #[test] fn test_search_suggestions_workflow() { assert!(true); }
417 #[test] fn test_search_filtering_workflow() { assert!(true); }
418
419 #[test] fn test_search_large_suggestion_lists() { assert!(true); }
421 #[test] fn test_search_render_performance() { assert!(true); }
422 #[test] fn test_search_memory_usage() { assert!(true); }
423 #[test] fn test_search_debounce_performance() { assert!(true); }
424}