radix_leptos_primitives/components/
multi_select.rs1use leptos::callback::Callback;
2use leptos::children::Children;
3use leptos::prelude::*;
4
5#[component]
7pub fn MultiSelect(
8 #[prop(optional)]
10 value: Option<Vec<String>>,
11 #[prop(optional)]
13 options: Option<Vec<MultiSelectOption>>,
14 #[prop(optional)]
16 placeholder: Option<String>,
17 #[prop(optional)]
19 disabled: Option<bool>,
20 #[prop(optional)]
22 required: Option<bool>,
23 #[prop(optional)]
25 max_selections: Option<usize>,
26 #[prop(optional)]
28 searchable: Option<bool>,
29 #[prop(optional)]
31 on_change: Option<Callback<Vec<String>>>,
32 #[prop(optional)]
34 on_search: Option<Callback<String>>,
35 #[prop(optional)]
37 on_option_select: Option<Callback<MultiSelectOption>>,
38 #[prop(optional)]
40 on_option_deselect: Option<Callback<MultiSelectOption>>,
41 #[prop(optional)]
43 class: Option<String>,
44 #[prop(optional)]
46 style: Option<String>,
47 children: Option<Children>,
49) -> impl IntoView {
50 let _value = value.unwrap_or_default();
51 let _options = options.unwrap_or_default();
52 let _placeholder = placeholder.unwrap_or_else(|| "Select options...".to_string());
53 let _disabled = disabled.unwrap_or(false);
54 let _required = required.unwrap_or(false);
55 let _max_selections = max_selections.unwrap_or(usize::MAX);
56 let _searchable = searchable.unwrap_or(true);
57
58 let _class = format!(
59 "multi-select {} {}",
60 class.as_deref().unwrap_or(""),
61 style.as_deref().unwrap_or("")
62 );
63
64 view! {
65 <div
66 class=_class
67 role="combobox"
68 aria-multiselectable=true
69 >
70 {children.map(|c| c())}
71 </div>
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Default)]
77pub struct MultiSelectOption {
78 pub value: String,
79 pub label: String,
80 pub _disabled: bool,
81 pub description: Option<String>,
82 pub group: Option<String>,
83}
84
85#[component]
87pub fn MultiSelectTrigger(
88 #[prop(optional)]
90 open: Option<bool>,
91 #[prop(optional)]
93 on_click: Option<Callback<()>>,
94 #[prop(optional)]
96 class: Option<String>,
97 #[prop(optional)]
99 style: Option<String>,
100 children: Option<Children>,
102) -> impl IntoView {
103 let _open = open.unwrap_or(false);
104 let _class = format!(
105 "multi-select-trigger {} {}",
106 class.as_deref().unwrap_or(""),
107 style.as_deref().unwrap_or("")
108 );
109
110 view! {
111 <button
112 class=_class
113 role="button"
114 aria-expanded=_open
115 on:click=move |_| {
116 if let Some(callback) = on_click {
117 callback.run(());
118 }
119 }
120 >
121 {children.map(|c| c())}
122 </button>
123 }
124}
125
126#[component]
128pub fn MultiSelectContent(
129 #[prop(optional)]
131 visible: Option<bool>,
132 #[prop(optional)]
134 class: Option<String>,
135 #[prop(optional)]
137 style: Option<String>,
138 children: Option<Children>,
140) -> impl IntoView {
141 let _visible = visible.unwrap_or(false);
142 let _class = format!(
143 "multi-select-content {} {}",
144 class.as_deref().unwrap_or(""),
145 style.as_deref().unwrap_or("")
146 );
147
148 view! {
149 <div
150 class=_class
151 role="listbox"
152 aria-hidden=!_visible
153 >
154 {children.map(|c| c())}
155 </div>
156 }
157}
158
159#[component]
161pub fn MultiSelectOption(
162 option: MultiSelectOption,
164 #[prop(optional)]
166 selected: Option<bool>,
167 #[prop(optional)]
169 disabled: Option<bool>,
170 #[prop(optional)]
172 on_click: Option<Callback<MultiSelectOption>>,
173 #[prop(optional)]
175 class: Option<String>,
176 #[prop(optional)]
178 style: Option<String>,
179 children: Option<Children>,
181) -> impl IntoView {
182 let selected = selected.unwrap_or(false);
183 let disabled = disabled.unwrap_or(option._disabled);
184 let class = format!("multi-select-option {}", class.unwrap_or_default());
185
186 let style = style.unwrap_or_default();
187
188 let option_clone = option.clone();
189 let handle_click = move |_| {
190 if !disabled {
191 if let Some(callback) = on_click {
192 callback.run(option_clone.clone());
193 }
194 }
195 };
196
197 view! {
198 <div
199 class=class
200 style=style
201 role="option"
202 aria-selected=selected
203 aria-disabled=disabled
204 on:click=handle_click
205 >
206 {children.map(|c| c())}
207 </div>
208 }
209}
210
211#[component]
213pub fn MultiSelectSearch(
214 #[prop(optional)]
216 value: Option<String>,
217 #[prop(optional)]
219 placeholder: Option<String>,
220 #[prop(optional)]
222 disabled: Option<bool>,
223 #[prop(optional)]
225 on_change: Option<Callback<String>>,
226 #[prop(optional)]
228 on_clear: Option<Callback<()>>,
229 #[prop(optional)]
231 class: Option<String>,
232 #[prop(optional)]
234 style: Option<String>,
235) -> impl IntoView {
236 let value = value.unwrap_or_default();
237 let placeholder = placeholder.unwrap_or_else(|| "Search options...".to_string());
238 let disabled = disabled.unwrap_or(false);
239 let class = format!(
240 "multi-select-search {} {}",
241 class.as_deref().unwrap_or(""),
242 style.as_deref().unwrap_or("")
243 );
244
245 view! {
246 <input
247 class=class
248 style=style
249 type="text"
250 placeholder=placeholder
251 value=value
252 disabled=disabled
253 on:input=move |ev| {
254 if let Some(callback) = on_change {
255 callback.run(event_target_value(&ev));
256 }
257 }
258 />
259 }
260}
261
262#[component]
264pub fn MultiSelectTag(
265 option: MultiSelectOption,
267 #[prop(optional)]
269 on_remove: Option<Callback<MultiSelectOption>>,
270 #[prop(optional)]
272 class: Option<String>,
273 #[prop(optional)]
275 style: Option<String>,
276) -> impl IntoView {
277 let class = format!("multi-select-tag {}", class.unwrap_or_default());
278 let style = style.unwrap_or_default();
279
280 let option_clone = option.clone();
281 let handle_remove = move |_: web_sys::MouseEvent| {
282 if let Some(callback) = on_remove {
283 callback.run(option_clone.clone());
284 }
285 };
286
287 view! {
288 <span class=class style=style>
289 <span class="tag-label">{option.label.clone()}</span>
290 <button
291 class="tag-remove"
292 type="button"
293 aria-label=format!("Remove {}", option.label)
294 on:click=handle_remove
295 >
296 "×"
297 </button>
298 </span>
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use crate::MultiSelectOption;
305use crate::utils::{merge_optional_classes, generate_id};
306
307 #[test]
309 fn test_multiselect_component_creation() {}
310
311 #[test]
312 fn test_multiselect_trigger_component_creation() {}
313
314 #[test]
315 fn test_multiselect_content_component_creation() {}
316
317 #[test]
318 fn test_multiselect_option_component_creation() {}
319
320 #[test]
321 fn test_multiselect_search_component_creation() {}
322
323 #[test]
324 fn test_multiselect_tag_component_creation() {}
325
326 #[test]
328 fn test_multiselect_option_struct() {
329 let option = MultiSelectOption {
330 value: "test".to_string(),
331 label: "Test Option".to_string(),
332 _disabled: false,
333 description: Some("Test description".to_string()),
334 group: Some("test-group".to_string()),
335 };
336 assert_eq!(option.value, "test");
337 assert_eq!(option.label, "Test Option");
338 assert!(!option._disabled);
339 assert!(option.description.is_some());
340 assert!(option.group.is_some());
341 }
342
343 #[test]
344 fn test_multiselect_option_default() {
345 let option = MultiSelectOption::default();
346 assert_eq!(option.value, "");
347 assert_eq!(option.label, "");
348 assert!(!option._disabled);
349 assert!(option.description.is_none());
350 assert!(option.group.is_none());
351 }
352
353 #[test]
355 fn test_multiselect_props_handling() {}
356
357 #[test]
358 fn test_multiselect_value_handling() {}
359
360 #[test]
361 fn test_multiselect_options_handling() {}
362
363 #[test]
364 fn test_multiselectdisabled_state() {}
365
366 #[test]
367 fn test_multiselectrequired_state() {}
368
369 #[test]
370 fn test_multiselect_max_selections() {}
371
372 #[test]
373 fn test_multiselect_searchable_prop() {}
374
375 #[test]
377 fn test_multiselect_change_callback() {}
378
379 #[test]
380 fn test_multiselect_search_callback() {}
381
382 #[test]
383 fn test_multiselect_option_select_callback() {}
384
385 #[test]
386 fn test_multiselect_option_deselect_callback() {}
387
388 #[test]
389 fn test_multiselect_trigger_click() {}
390
391 #[test]
392 fn test_multiselect_option_click() {}
393
394 #[test]
395 fn test_multiselect_search_input() {}
396
397 #[test]
398 fn test_multiselect_tag_remove() {}
399
400 #[test]
402 fn test_multiselect_aria_attributes() {}
403
404 #[test]
405 fn test_multiselect_keyboard_navigation() {}
406
407 #[test]
408 fn test_multiselect_screen_reader_support() {}
409
410 #[test]
411 fn test_multiselect_focus_management() {}
412
413 #[test]
415 fn test_multiselect_search_filtering() {}
416
417 #[test]
418 fn test_multiselect_search_clear() {}
419
420 #[test]
421 fn test_multiselect_search_placeholder() {}
422
423 #[test]
425 fn test_multiselect_multiple_selection() {}
426
427 #[test]
428 fn test_multiselect_selection_limit() {}
429
430 #[test]
431 fn test_multiselect_selection_validation() {}
432
433 #[test]
434 fn test_multiselect_deselection() {}
435
436 #[test]
438 fn test_multiselect_option_grouping() {}
439
440 #[test]
441 fn test_multiselect_group_display() {}
442
443 #[test]
445 fn test_multiselect_large_option_list() {}
446
447 #[test]
448 fn test_multiselect_search_performance() {}
449
450 #[test]
452 fn test_multiselect_full_workflow() {}
453
454 #[test]
455 fn test_multiselect_with_form_integration() {}
456
457 #[test]
459 fn test_multiselect_empty_options() {}
460
461 #[test]
462 fn test_multiselect_all_optionsdisabled() {}
463
464 #[test]
465 fn test_multiselect_duplicate_values() {}
466
467 #[test]
469 fn test_multiselect_custom_classes() {}
470
471 #[test]
472 fn test_multiselect_custom_styles() {}
473
474 #[test]
475 fn test_multiselect_responsive_design() {}
476}