radix_leptos_primitives/components/
tree_view.rs1use leptos::callback::Callback;
2use leptos::children::Children;
3use leptos::prelude::*;
4
5#[component]
7pub fn TreeView(
8 #[prop(optional)]
10 data: Option<Vec<TreeNode>>,
11 #[prop(optional)]
13 show_icons: Option<bool>,
14 #[prop(optional)]
16 multiple: Option<bool>,
17 #[prop(optional)]
19 checkable: Option<bool>,
20 #[prop(optional)]
22 show_lines: Option<bool>,
23 #[prop(optional)]
25 show_node_icons: Option<bool>,
26 #[prop(optional)]
28 on_select: Option<Callback<TreeNode>>,
29 #[prop(optional)]
31 on_expand: Option<Callback<TreeNode>>,
32 #[prop(optional)]
34 on_check: Option<Callback<TreeNode>>,
35 #[prop(optional)]
37 class: Option<String>,
38 #[prop(optional)]
40 style: Option<String>,
41 #[prop(optional)]
43 children: Option<Children>,
44) -> impl IntoView {
45 let data = data.unwrap_or_default();
46 let show_icons = show_icons.unwrap_or(true);
47 let multiple = multiple.unwrap_or(false);
48 let checkable = checkable.unwrap_or(false);
49 let show_lines = show_lines.unwrap_or(false);
50 let show_node_icons = show_node_icons.unwrap_or(true);
51
52 let class = "tree-view".to_string();
53
54 let style = style.unwrap_or_default();
55
56 view! {
57 <div class=class style=style role="tree">
58 {children.map(|c| c())}
59 </div>
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Default)]
65pub struct TreeNode {
66 pub id: String,
67 pub label: String,
68 pub value: Option<String>,
69 pub icon: Option<String>,
70 pub children: Option<Vec<TreeNode>>,
71 pub expanded: bool,
72 pub _selected: bool,
73 pub _checked: bool,
74 pub _disabled: bool,
75 pub level: usize,
76 pub parent_id: Option<String>,
77}
78
79#[component]
81pub fn TreeNode(
82 node: TreeNode,
84 #[prop(optional)]
86 show_icons: Option<bool>,
87 #[prop(optional)]
89 multiple: Option<bool>,
90 #[prop(optional)]
92 checkable: Option<bool>,
93 #[prop(optional)]
95 show_lines: Option<bool>,
96 #[prop(optional)]
98 show_node_icons: Option<bool>,
99 #[prop(optional)]
101 on_select: Option<Callback<TreeNode>>,
102 #[prop(optional)]
104 on_expand: Option<Callback<TreeNode>>,
105 #[prop(optional)]
107 on_check: Option<Callback<TreeNode>>,
108 #[prop(optional)]
110 class: Option<String>,
111 #[prop(optional)]
113 style: Option<String>,
114 #[prop(optional)]
116 children: Option<Children>,
117) -> impl IntoView {
118 let show_icons = show_icons.unwrap_or(true);
119 let multiple = multiple.unwrap_or(false);
120 let checkable = checkable.unwrap_or(false);
121 let show_lines = show_lines.unwrap_or(false);
122 let show_node_icons = show_node_icons.unwrap_or(true);
123
124 let class = format!(
125 "tree-node {} {} {} {} {}",
126 if node.expanded {
127 "expanded"
128 } else {
129 "collapsed"
130 },
131 if node._selected {
132 "selected"
133 } else {
134 "unselected"
135 },
136 if node._disabled {
137 "disabled"
138 } else {
139 "enabled"
140 },
141 node.level * 20,
142 style.clone().unwrap_or_default()
143 );
144
145 let node_clone = node.clone();
146 let handle_select = move |_| {
147 if !node_clone._disabled {
148 if let Some(callback) = on_select {
149 callback.run(node_clone.clone());
150 }
151 }
152 };
153
154 let node_clone = node.clone();
155 let handle_expand = move |_: ()| {
156 if !node_clone._disabled {
157 if let Some(callback) = on_expand {
158 callback.run(node_clone.clone());
159 }
160 }
161 };
162
163 let node_clone = node.clone();
164 let handle_check = move |_| {
165 if !node_clone._disabled {
166 if let Some(callback) = on_check {
167 callback.run(node_clone.clone());
168 }
169 }
170 };
171
172 view! {
173 <div class=class style=style role="treeitem" aria-expanded=node.expanded aria-selected=node._selected>
174 <div class="tree-node-content">
175 {if show_icons && node.children.is_some() {
176 view! {
177 <button
178 class="tree-expand-icon"
179 type="button"
180 >
181 </button>
182 }.into_any()
183 } else {
184 view! { <div></div> }.into_any()
185 }}
186
187 {if checkable {
188 view! {
189 <input
190 class="tree-checkbox"
191 type="checkbox"
192 checked=node._checked
193 disabled=node._disabled
194 on:change=handle_check
195 />
196 }.into_any()
197 } else {
198 view! { <div></div> }.into_any()
199 }}
200
201 {if show_node_icons && node.icon.is_some() {
202 view! {
203 <span class="tree-node-icon">{node.icon.clone().unwrap()}</span>
204 }.into_any()
205 } else {
206 view! { <div></div> }.into_any()
207 }}
208
209 <span class="tree-node-label" on:click=handle_select>
210 {node.label.clone()}
211 </span>
212 </div>
213
214 {if node.expanded && node.children.is_some() {
215 view! {
216 <div class="tree-children" role="group">
217 {node.children.clone().unwrap().into_iter().map(|child| {
218 view! {
219 <TreeNode
220 node=child
221 show_icons=show_icons
222 multiple=multiple
223 checkable=checkable
224 show_lines=show_lines
225 show_node_icons=show_node_icons
226 on_select=on_select.unwrap_or_else(|| Callback::new(|_| {}))
227 on_expand=on_expand.unwrap_or_else(|| Callback::new(|_| {}))
228 on_check=on_check.unwrap_or_else(|| Callback::new(|_| {}))
229 >
230 <></>
231 </TreeNode>
232 }
233 }).collect::<Vec<_>>()}
234 </div>
235 }.into_any()
236 } else {
237 view! { <div></div> }.into_any()
238 }}
239
240 {children.map(|c| c())}
241 </div>
242 }
243}
244
245#[component]
247pub fn TreeViewSearch(
248 #[prop(optional)]
250 value: Option<String>,
251 #[prop(optional)]
253 placeholder: Option<String>,
254 #[prop(optional)]
256 disabled: Option<bool>,
257 #[prop(optional)]
259 on_change: Option<Callback<String>>,
260 #[prop(optional)]
262 on_clear: Option<Callback<()>>,
263 #[prop(optional)]
265 class: Option<String>,
266 #[prop(optional)]
268 style: Option<String>,
269 #[prop(optional)]
271 children: Option<Children>,
272) -> impl IntoView {
273 let value = value.unwrap_or_default();
274 let placeholder = placeholder.unwrap_or_else(|| "Search tree...".to_string());
275 let disabled = disabled.unwrap_or(false);
276 let class = format!(
277 "tree-search {} {}",
278 class.as_deref().unwrap_or(""),
279 style.as_deref().unwrap_or("")
280 );
281
282 view! {
283 <input
284 class=class
285 style=style
286 type="text"
287 placeholder=placeholder
288 value=value
289 disabled=disabled
290 on:input=move |ev| {
291 if let Some(callback) = on_change {
292 callback.run(event_target_value(&ev));
293 }
294 }
295 />
296 }
297}
298
299#[component]
301pub fn TreeViewActions(
302 #[prop(optional)]
304 on_expand_all: Option<Callback<()>>,
305 #[prop(optional)]
307 on_collapse_all: Option<Callback<()>>,
308 #[prop(optional)]
310 on_select_all: Option<Callback<()>>,
311 #[prop(optional)]
313 on_deselect_all: Option<Callback<()>>,
314 #[prop(optional)]
316 class: Option<String>,
317 #[prop(optional)]
319 style: Option<String>,
320 #[prop(optional)]
322 children: Option<Children>,
323) -> impl IntoView {
324 let class = format!("tree-actions {}", class.unwrap_or_default());
325 let style = style.unwrap_or_default();
326
327 view! {
328 <div class=class style=style>
329 {children.map(|c| c())}
330 </div>
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use crate::TreeNode;
337use crate::utils::{merge_optional_classes, generate_id};
338
339 #[test]
341 fn test_treeview_component_creation() {}
342
343 #[test]
344 fn test_treenode_component_creation() {}
345
346 #[test]
347 fn test_treeview_search_component_creation() {}
348
349 #[test]
350 fn test_treeview_actions_component_creation() {}
351
352 #[test]
354 fn test_treenode_struct() {
355 let node = TreeNode {
356 id: "node1".to_string(),
357 label: "Node 1".to_string(),
358 value: Some("value1".to_string()),
359 icon: Some("📁".to_string()),
360 children: Some(Vec::new()),
361 expanded: false,
362 _selected: false,
363 _checked: false,
364 _disabled: false,
365 level: 0,
366 parent_id: None,
367 };
368 assert_eq!(node.id, "node1");
369 assert_eq!(node.label, "Node 1");
370 assert!(node.value.is_some());
371 assert!(node.icon.is_some());
372 assert!(node.children.is_some());
373 assert!(!node.expanded);
374 assert!(!node._selected);
375 assert!(!node._checked);
376 assert!(!node._disabled);
377 assert_eq!(node.level, 0);
378 assert!(node.parent_id.is_none());
379 }
380
381 #[test]
382 fn test_treenode_default() {
383 let node = TreeNode::default();
384 assert_eq!(node.id, "");
385 assert_eq!(node.label, "");
386 assert!(node.value.is_none());
387 assert!(node.icon.is_none());
388 assert!(node.children.is_none());
389 assert!(!node.expanded);
390 assert!(!node._selected);
391 assert!(!node._checked);
392 assert!(!node._disabled);
393 assert_eq!(node.level, 0);
394 assert!(node.parent_id.is_none());
395 }
396
397 #[test]
399 fn test_treeview_props_handling() {}
400
401 #[test]
402 fn test_treeview_data_handling() {}
403
404 #[test]
405 fn test_treeview_show_icons() {}
406
407 #[test]
408 fn test_treeview_multiple_selection_2() {}
409
410 #[test]
411 fn test_treeview_checkable() {}
412
413 #[test]
414 fn test_treeview_show_lines() {}
415
416 #[test]
417 fn test_treeview_show_node_icons() {}
418
419 #[test]
421 fn test_treeview_node_select() {}
422
423 #[test]
424 fn test_treeview_node_expand() {}
425
426 #[test]
427 fn test_treeview_node_check() {}
428
429 #[test]
430 fn test_treeview_search_change() {}
431
432 #[test]
433 fn test_treeview_search_clear() {}
434
435 #[test]
436 fn test_treeview_expand_all() {}
437
438 #[test]
439 fn test_treeview_collapse_all() {}
440
441 #[test]
442 fn test_treeview_select_all() {}
443
444 #[test]
445 fn test_treeview_deselect_all() {}
446
447 #[test]
449 fn test_treeview_aria_attributes() {}
450
451 #[test]
452 fn test_treeview_keyboard_navigation() {}
453
454 #[test]
455 fn test_treeview_screen_reader_support() {}
456
457 #[test]
458 fn test_treeview_focus_management() {}
459
460 #[test]
462 fn test_treeview_nested_structure() {}
463
464 #[test]
465 fn test_treeview_node_levels() {}
466
467 #[test]
468 fn test_treeview_parent_child_relationships() {}
469
470 #[test]
472 fn test_treeview_expand_node() {}
473
474 #[test]
475 fn test_treeview_collapse_node() {}
476
477 #[test]
478 fn test_treeview_expand_all_nodes() {}
479
480 #[test]
481 fn test_treeview_collapse_all_nodes() {}
482
483 #[test]
485 fn test_treeview_single_selection() {}
486
487 #[test]
488 fn test_treeview_checkbox_selection() {}
489
490 #[test]
491 fn test_treeview_selection_state() {}
492
493 #[test]
495 fn test_treeview_search_filtering() {}
496
497 #[test]
498 fn test_treeview_search_highlighting() {}
499
500 #[test]
501 fn test_treeview_search_expand_matches() {}
502
503 #[test]
505 fn test_treeview_large_dataset() {}
506
507 #[test]
508 fn test_treeview_deep_nesting() {}
509
510 #[test]
511 fn test_treeview_rendering_performance() {}
512
513 #[test]
515 fn test_treeview_full_workflow() {}
516
517 #[test]
518 fn test_treeview_with_search() {}
519
520 #[test]
521 fn test_treeview_with_actions() {}
522
523 #[test]
525 fn test_treeview_empty_data() {}
526
527 #[test]
528 fn test_treeview_single_node() {}
529
530 #[test]
531 fn test_treeviewdisabled_nodes() {}
532
533 #[test]
534 fn test_treeview_duplicate_ids() {}
535
536 #[test]
538 fn test_treeview_custom_classes() {}
539
540 #[test]
541 fn test_treeview_custom_styles() {}
542
543 #[test]
544 fn test_treeview_responsive_design() {}
545
546 #[test]
547 fn test_treeview_icon_display() {}
548
549 #[test]
550 fn test_treeview_line_display() {}
551}