radix_leptos_primitives/components/
tree_view.rs

1use leptos::callback::Callback;
2use leptos::children::Children;
3use leptos::prelude::*;
4
5/// Tree View component for displaying hierarchical data
6#[component]
7pub fn TreeView(
8    /// Tree data
9    #[prop(optional)]
10    data: Option<Vec<TreeNode>>,
11    /// Whether to show expand/collapse icons
12    #[prop(optional)]
13    show_icons: Option<bool>,
14    /// Whether to allow multiple selection
15    #[prop(optional)]
16    multiple: Option<bool>,
17    /// Whether to allow checkbox selection
18    #[prop(optional)]
19    checkable: Option<bool>,
20    /// Whether to show lines connecting nodes
21    #[prop(optional)]
22    show_lines: Option<bool>,
23    /// Whether to show node icons
24    #[prop(optional)]
25    show_node_icons: Option<bool>,
26    /// Callback when node is selected
27    #[prop(optional)]
28    on_select: Option<Callback<TreeNode>>,
29    /// Callback when node is expanded/collapsed
30    #[prop(optional)]
31    on_expand: Option<Callback<TreeNode>>,
32    /// Callback when node is checked/unchecked
33    #[prop(optional)]
34    on_check: Option<Callback<TreeNode>>,
35    /// Additional CSS classes
36    #[prop(optional)]
37    class: Option<String>,
38    /// Inline styles
39    #[prop(optional)]
40    style: Option<String>,
41    /// Children content
42    #[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/// Tree Node structure
64#[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/// Tree Node component
80#[component]
81pub fn TreeNode(
82    /// Node data
83    node: TreeNode,
84    /// Whether to show expand/collapse icons
85    #[prop(optional)]
86    show_icons: Option<bool>,
87    /// Whether to allow multiple selection
88    #[prop(optional)]
89    multiple: Option<bool>,
90    /// Whether to allow checkbox selection
91    #[prop(optional)]
92    checkable: Option<bool>,
93    /// Whether to show lines connecting nodes
94    #[prop(optional)]
95    show_lines: Option<bool>,
96    /// Whether to show node icons
97    #[prop(optional)]
98    show_node_icons: Option<bool>,
99    /// Callback when node is selected
100    #[prop(optional)]
101    on_select: Option<Callback<TreeNode>>,
102    /// Callback when node is expanded/collapsed
103    #[prop(optional)]
104    on_expand: Option<Callback<TreeNode>>,
105    /// Callback when node is checked/unchecked
106    #[prop(optional)]
107    on_check: Option<Callback<TreeNode>>,
108    /// Additional CSS classes
109    #[prop(optional)]
110    class: Option<String>,
111    /// Inline styles
112    #[prop(optional)]
113    style: Option<String>,
114    /// Children content
115    #[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/// Tree View Search component
246#[component]
247pub fn TreeViewSearch(
248    /// Search query value
249    #[prop(optional)]
250    value: Option<String>,
251    /// Placeholder text
252    #[prop(optional)]
253    placeholder: Option<String>,
254    /// Whether the search is disabled
255    #[prop(optional)]
256    disabled: Option<bool>,
257    /// Callback when search query changes
258    #[prop(optional)]
259    on_change: Option<Callback<String>>,
260    /// Callback when search is cleared
261    #[prop(optional)]
262    on_clear: Option<Callback<()>>,
263    /// Additional CSS classes
264    #[prop(optional)]
265    class: Option<String>,
266    /// Inline styles
267    #[prop(optional)]
268    style: Option<String>,
269    /// Children content
270    #[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/// Tree View Actions component
300#[component]
301pub fn TreeViewActions(
302    /// Callback when expand all is clicked
303    #[prop(optional)]
304    on_expand_all: Option<Callback<()>>,
305    /// Callback when collapse all is clicked
306    #[prop(optional)]
307    on_collapse_all: Option<Callback<()>>,
308    /// Callback when select all is clicked
309    #[prop(optional)]
310    on_select_all: Option<Callback<()>>,
311    /// Callback when deselect all is clicked
312    #[prop(optional)]
313    on_deselect_all: Option<Callback<()>>,
314    /// Additional CSS classes
315    #[prop(optional)]
316    class: Option<String>,
317    /// Inline styles
318    #[prop(optional)]
319    style: Option<String>,
320    /// Children content
321    #[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    // Component structure tests
340    #[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    // Data structure tests
353    #[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    // Props and state tests
398    #[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    // Event handling tests
420    #[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    // Accessibility tests
448    #[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    // Hierarchical data tests
461    #[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    // Expand/collapse tests
471    #[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    // Selection tests
484    #[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    // Search functionality tests
494    #[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    // Performance tests
504    #[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    // Integration tests
514    #[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    // Edge case tests
524    #[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    // Styling tests
537    #[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}