orbital_core_components/tree/tree.rs
1use leptos::prelude::*;
2use orbital_base_components::BaseTreeRoot;
3use orbital_macros::component_doc;
4use orbital_style::inject_style;
5
6use super::api::{build_tree_state, install_tree_reorder_listeners, TreeApiHandle, TreeApiRef};
7use super::styles::tree_styles;
8use super::types::{TreeAppearance, TreeBehavior, TreeExpansion, TreeSelection};
9
10/// Hand-composed hierarchy for sidebars, pickers, and file lists where you control every row.
11///
12/// Wrap rows in nested [`TreeItem`] components with [`TreeItemLayout`] slots. Bind [`TreeExpansion`] and [`TreeSelection`] with Leptos signals for open and selected ids. When items come from an API, lazy loading, or virtualization, use [`RichTree`](super::rich_tree::RichTree) instead — same selection and expansion APIs, data adapters instead of nested components.
13///
14/// # When to use
15///
16/// - Hand-authored hierarchies with custom slots (icons, aside actions, motion) - Sidebars and pickers where item count is modest - Full control over row layout via [`TreeItemLayout`]
17///
18/// # Usage
19///
20/// 1. Wrap rows in [`Tree`] + nested [`TreeItem`] components. 2. Bind [`TreeExpansion::open_items`] for controlled expand/collapse. 3. Choose [`TreeSelection`] mode (`single`, `multi`, `checkbox`, or `none`).
21///
22/// # Best Practices
23///
24/// ## Do's
25///
26/// * Use [`TreeApiRef`](super::api::TreeApiRef) to focus items from external controls * Set `data-testid` on preview wrappers for E2E coverage * Prefer [`RichTree`](super::rich_tree::RichTree) when items come from an API
27///
28/// ## Don'ts
29///
30/// * Don't render thousands of static [`TreeItem`] nodes — use [`RichTree`](super::rich_tree::RichTree) with virtualization * Don't rely on mount order for keyboard range selection — the registry orders visible items
31///
32/// # Examples
33///
34/// ## Default nested tree
35/// Branch pre-expanded via `open_items`; nested leaf under Documents.
36/// <!-- preview -->
37/// ```rust
38/// use crate::{Tree, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
39/// use orbital_base_components::SignalModel;
40/// use std::collections::HashSet;
41/// let open = RwSignal::new(HashSet::from(["root".to_string()]));
42/// view! {
43/// <div data-testid="tree-view-preview">
44/// <Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
45/// <TreeItem config=TreeItemConfig::branch("root")>
46/// <TreeItemLayout slot>
47/// <span data-testid="tree-node-docs">"Documents"</span>
48/// </TreeItemLayout>
49/// <TreeItem config=TreeItemConfig::leaf("readme")>
50/// <TreeItemLayout slot>
51/// <span data-testid="tree-node-readme">"readme.md"</span>
52/// </TreeItemLayout>
53/// </TreeItem>
54/// </TreeItem>
55/// </Tree>
56/// </div>
57/// }
58/// ```
59///
60/// ## Small size
61/// Compact row height for dense sidebars.
62/// <!-- preview -->
63/// ```rust
64/// use crate::{Tree, TreeAppearance, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection, TreeSize};
65/// use orbital_base_components::SignalModel;
66/// use std::collections::HashSet;
67/// let open = RwSignal::new(HashSet::new());
68/// let appearance = TreeAppearance::new(Signal::derive(|| TreeSize::Small));
69/// view! {
70/// <div data-testid="tree-small">
71/// <Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none() appearance=appearance>
72/// <TreeItem config=TreeItemConfig::leaf("a")>
73/// <TreeItemLayout slot>"Item"</TreeItemLayout>
74/// </TreeItem>
75/// </Tree>
76/// </div>
77/// }
78/// ```
79///
80/// ## Deep nesting
81/// Three-level branch chain demonstrates nested subtrees.
82/// <!-- preview -->
83/// ```rust
84/// use crate::{Tree, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
85/// use orbital_base_components::SignalModel;
86/// use std::collections::HashSet;
87/// let open = RwSignal::new(HashSet::from(["l1".to_string(), "l2".to_string()]));
88/// view! {
89/// <div data-testid="tree-deep">
90/// <Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
91/// <TreeItem config=TreeItemConfig::branch("l1")>
92/// <TreeItemLayout slot>"Level 1"</TreeItemLayout>
93/// <TreeItem config=TreeItemConfig::branch("l2")>
94/// <TreeItemLayout slot>"Level 2"</TreeItemLayout>
95/// <TreeItem config=TreeItemConfig::leaf("l3")>
96/// <TreeItemLayout slot>"Level 3"</TreeItemLayout>
97/// </TreeItem>
98/// </TreeItem>
99/// </TreeItem>
100/// </Tree>
101/// </div>
102/// }
103/// ```
104///
105/// ## Click to expand
106/// Starts collapsed; clicking the branch toggles `open_items`.
107/// <!-- preview -->
108/// ```rust
109/// use crate::{Tree, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
110/// use orbital_base_components::SignalModel;
111/// use std::collections::HashSet;
112/// let open = RwSignal::new(HashSet::new());
113/// view! {
114/// <div data-testid="tree-toggle">
115/// <Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
116/// <TreeItem config=TreeItemConfig::branch("toggle")>
117/// <TreeItemLayout slot>
118/// <span data-testid="tree-branch-toggle">"Expand me"</span>
119/// </TreeItemLayout>
120/// <TreeItem config=TreeItemConfig::leaf("child")>
121/// <TreeItemLayout slot>"Hidden until expanded"</TreeItemLayout>
122/// </TreeItem>
123/// </TreeItem>
124/// </Tree>
125/// </div>
126/// }
127/// ```
128///
129/// ## Flat leaf list
130/// All leaves — no chevrons.
131/// <!-- preview -->
132/// ```rust
133/// use crate::{Tree, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
134/// use orbital_base_components::SignalModel;
135/// use std::collections::HashSet;
136/// let open = RwSignal::new(HashSet::new());
137/// view! {
138/// <div data-testid="tree-flat">
139/// <Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
140/// <TreeItem config=TreeItemConfig::leaf("1")>
141/// <TreeItemLayout slot>"One"</TreeItemLayout>
142/// </TreeItem>
143/// <TreeItem config=TreeItemConfig::leaf("2")>
144/// <TreeItemLayout slot>"Two"</TreeItemLayout>
145/// </TreeItem>
146/// </Tree>
147/// </div>
148/// }
149/// ```
150///
151/// ## Single selection
152/// Click rows to select one item at a time.
153/// <!-- preview -->
154/// ```rust
155/// use crate::{Tree, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
156/// use orbital_base_components::{SignalModel, TreeSelectionMode};
157/// use std::collections::HashSet;
158/// let open = RwSignal::new(HashSet::new());
159/// let selected = RwSignal::new(HashSet::new());
160/// view! {
161/// <div data-testid="tree-selection">
162/// <Tree
163/// expansion=TreeExpansion::new(SignalModel::from(open))
164/// selection=TreeSelection::single(SignalModel::from(selected))
165/// >
166/// <TreeItem config=TreeItemConfig::leaf("a")>
167/// <TreeItemLayout slot>"Alpha"</TreeItemLayout>
168/// </TreeItem>
169/// <TreeItem config=TreeItemConfig::leaf("b")>
170/// <TreeItemLayout slot>"Beta"</TreeItemLayout>
171/// </TreeItem>
172/// </Tree>
173/// </div>
174/// }
175/// ```
176///
177/// ## Theme surfaces
178/// Items use theme background tokens.
179/// <!-- preview -->
180/// ```rust
181/// use crate::{Tree, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
182/// use orbital_base_components::SignalModel;
183/// use std::collections::HashSet;
184/// let open = RwSignal::new(HashSet::from(["theme".to_string()]));
185/// view! {
186/// <div data-testid="tree-theme">
187/// <Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
188/// <TreeItem config=TreeItemConfig::branch("theme")>
189/// <TreeItemLayout slot>
190/// <span data-testid="tree-theme-cell" style="background: var(--orb-color-surface-canvas); padding: 4px 8px;">"Themed row"</span>
191/// </TreeItemLayout>
192/// </TreeItem>
193/// </Tree>
194/// </div>
195/// }
196/// ```
197///
198/// ## RichTree data-driven
199/// Pass structured data instead of composing nested items.
200/// <!-- code-only -->
201/// ```rust
202/// use crate::{RichTree, RichTreeData, TreeExpansion, TreeSelection};
203/// use orbital_base_components::SignalModel;
204/// use std::collections::HashSet;
205/// #[derive(Clone)]
206/// struct Node { id: String, label: String, children: Vec<Node> }
207/// let data = RichTreeData::new(vec![
208/// Node { id: "docs".into(), label: "Documents".into(), children: vec![
209/// Node { id: "readme".into(), label: "readme.md".into(), children: vec![] },
210/// ]},
211/// ]).get_id(|n| n.id.clone()).get_label(|n| n.label.clone()).get_children(|n| n.children.clone());
212/// let open = RwSignal::new(HashSet::from(["docs".to_string()]));
213/// view! {
214/// <div data-testid="tree-rich">
215/// <RichTree data=data expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none() />
216/// </div>
217/// }
218/// ```
219///
220/// ## Icons and aside actions
221/// Leading icon, trailing badge, and hover-revealed aside actions.
222/// <!-- preview -->
223/// ```rust
224/// use crate::{Button, ButtonAppearance, Tree, TreeExpansion, TreeItem, TreeItemConfig, TreeItemIconAfter, TreeItemIconBefore, TreeItemAside, TreeItemLayout, TreeSelection};
225/// use orbital_base_components::SignalModel;
226/// use std::collections::HashSet;
227/// let open = RwSignal::new(HashSet::from(["docs".to_string()]));
228/// view! {
229/// <div data-testid="tree-icons-aside">
230/// <Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
231/// <TreeItem config=TreeItemConfig::branch("docs")>
232/// <TreeItemLayout slot>
233/// <TreeItemIconBefore slot>
234/// <span>"📁"</span>
235/// </TreeItemIconBefore>
236/// "Documents"
237/// <TreeItemIconAfter slot>
238/// <span>"3"</span>
239/// </TreeItemIconAfter>
240/// <TreeItemAside slot>
241/// <Button appearance=ButtonAppearance::Subtle icon=icondata::AiEllipsisOutlined />
242/// </TreeItemAside>
243/// </TreeItemLayout>
244/// <TreeItem config=TreeItemConfig::leaf("readme")>
245/// <TreeItemLayout slot>"readme.md"</TreeItemLayout>
246/// </TreeItem>
247/// </TreeItem>
248/// </Tree>
249/// </div>
250/// }
251/// ```
252///
253/// ## Multi selection (ctrl + shift)
254/// Ctrl/Meta toggles; Shift selects a visible range.
255/// <!-- preview -->
256/// ```rust
257/// use crate::{Tree, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
258/// use orbital_base_components::{SignalModel, TreeSelectionMode};
259/// use std::collections::HashSet;
260/// let open = RwSignal::new(HashSet::new());
261/// let selected = RwSignal::new(HashSet::new());
262/// view! {
263/// <div data-testid="tree-multi">
264/// <Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::multi(SignalModel::from(selected))>
265/// <TreeItem config=TreeItemConfig::leaf("a")><TreeItemLayout slot>"Alpha"</TreeItemLayout></TreeItem>
266/// <TreeItem config=TreeItemConfig::leaf("b")><TreeItemLayout slot>"Beta"</TreeItemLayout></TreeItem>
267/// <TreeItem config=TreeItemConfig::leaf("c")><TreeItemLayout slot>"Gamma"</TreeItemLayout></TreeItem>
268/// </Tree>
269/// </div>
270/// }
271/// ```
272///
273/// ## Checkbox selection with cascade
274/// Parent checkbox selects descendants when `cascade` is enabled.
275/// <!-- preview -->
276/// ```rust
277/// use crate::{Tree, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
278/// use orbital_base_components::SignalModel;
279/// use std::collections::HashSet;
280/// let open = RwSignal::new(HashSet::from(["root".to_string()]));
281/// let selected = RwSignal::new(HashSet::new());
282/// let selection = TreeSelection::checkbox(SignalModel::from(selected)).with_cascade(true);
283/// view! {
284/// <div data-testid="tree-checkbox">
285/// <Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=selection>
286/// <TreeItem config=TreeItemConfig::branch("root")>
287/// <TreeItemLayout slot>"Root"</TreeItemLayout>
288/// <TreeItem config=TreeItemConfig::leaf("child")><TreeItemLayout slot>"Child"</TreeItemLayout></TreeItem>
289/// </TreeItem>
290/// </Tree>
291/// </div>
292/// }
293/// ```
294///
295/// ## Disabled items (focusable)
296/// Disabled rows remain focusable for screen-reader access.
297/// <!-- preview -->
298/// ```rust
299/// use crate::{Tree, TreeBehavior, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
300/// use orbital_base_components::SignalModel;
301/// use std::collections::HashSet;
302/// let open = RwSignal::new(HashSet::new());
303/// let behavior = TreeBehavior::new()
304/// .with_disabled_items(SignalModel::new(HashSet::from(["b".to_string()])))
305/// .with_disabled_items_focusable(Signal::from(true));
306/// view! {
307/// <div data-testid="tree-disabled">
308/// <Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none() behavior=behavior>
309/// <TreeItem config=TreeItemConfig::leaf("a")><TreeItemLayout slot>"Enabled"</TreeItemLayout></TreeItem>
310/// <TreeItem config=TreeItemConfig::leaf("b")><TreeItemLayout slot>"Disabled"</TreeItemLayout></TreeItem>
311/// </Tree>
312/// </div>
313/// }
314/// ```
315///
316/// ## Icon-container expansion
317/// Only the chevron toggles expansion; row click selects.
318/// <!-- preview -->
319/// ```rust
320/// use crate::{Tree, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
321/// use orbital_base_components::{ExpansionTrigger, SignalModel};
322/// use std::collections::HashSet;
323/// let open = RwSignal::new(HashSet::new());
324/// let selected = RwSignal::new(HashSet::new());
325/// let expansion = TreeExpansion::new(SignalModel::from(open)).with_expansion_trigger(ExpansionTrigger::IconContainer);
326/// view! {
327/// <div data-testid="tree-icon-expand">
328/// <Tree expansion=expansion selection=TreeSelection::single(SignalModel::from(selected))>
329/// <TreeItem config=TreeItemConfig::branch("branch")>
330/// <TreeItemLayout slot><span data-testid="tree-icon-branch">"Branch"</span></TreeItemLayout>
331/// <TreeItem config=TreeItemConfig::leaf("leaf")><TreeItemLayout slot>"Leaf"</TreeItemLayout></TreeItem>
332/// </TreeItem>
333/// </Tree>
334/// </div>
335/// }
336/// ```
337///
338/// ## Expand / collapse all
339/// External button uses [`TreeApiRef`](super::api::TreeApiRef) to toggle every branch.
340/// <!-- preview -->
341/// ```rust
342/// use crate::{Button, Tree, TreeApiRef, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
343/// use orbital_base_components::SignalModel;
344/// use std::collections::HashSet;
345/// let open = RwSignal::new(HashSet::new());
346/// let api = TreeApiRef::default();
347/// let api_for_button = api.clone();
348/// view! {
349/// <div data-testid="tree-expand-all">
350/// <Button on_click=Callback::new(move |_| {
351/// api_for_button.with(|handle| {
352/// for (id, _) in handle.get_item_tree() {
353/// handle.set_item_expansion(id, true);
354/// }
355/// });
356/// })>"Expand all"</Button>
357/// <Tree api_ref=api expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
358/// <TreeItem config=TreeItemConfig::branch("a")>
359/// <TreeItemLayout slot>"A"</TreeItemLayout>
360/// <TreeItem config=TreeItemConfig::leaf("a1")><TreeItemLayout slot>"A1"</TreeItemLayout></TreeItem>
361/// </TreeItem>
362/// </Tree>
363/// </div>
364/// }
365/// ```
366///
367/// ## Connector borders
368/// Indentation guide lines between levels.
369/// <!-- preview -->
370/// ```rust
371/// use crate::{Tree, TreeAppearance, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
372/// use orbital_base_components::SignalModel;
373/// use std::collections::HashSet;
374/// let open = RwSignal::new(HashSet::from(["root".to_string()]));
375/// let appearance = TreeAppearance::new(Signal::from(orbital_base_components::TreeSize::Medium)).with_connectors(Signal::from(true));
376/// view! {
377/// <div data-testid="tree-connectors">
378/// <Tree appearance=appearance expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
379/// <TreeItem config=TreeItemConfig::branch("root")>
380/// <TreeItemLayout slot>"Root"</TreeItemLayout>
381/// <TreeItem config=TreeItemConfig::leaf("leaf")><TreeItemLayout slot>"Leaf"</TreeItemLayout></TreeItem>
382/// </TreeItem>
383/// </Tree>
384/// </div>
385/// }
386/// ```
387///
388/// ## Focus via button
389/// Imperative focus through [`TreeApiRef::focus_item`].
390/// <!-- preview -->
391/// ```rust
392/// use crate::{Button, Tree, TreeApiRef, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
393/// use orbital_base_components::SignalModel;
394/// use std::collections::HashSet;
395/// let open = RwSignal::new(HashSet::new());
396/// let api = TreeApiRef::default();
397/// let api_for_button = api.clone();
398/// view! {
399/// <div data-testid="tree-focus-button">
400/// <Button on_click=Callback::new(move |_| { api_for_button.with(|handle| handle.focus_item("target".into())); })>"Focus target"</Button>
401/// <Tree api_ref=api expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
402/// <TreeItem config=TreeItemConfig::leaf("target")><TreeItemLayout slot>"Target row"</TreeItemLayout></TreeItem>
403/// </Tree>
404/// </div>
405/// }
406/// ```
407///
408/// ## Keyboard navigation
409/// Arrow keys move focus; typeahead jumps to matching labels.
410/// <!-- preview -->
411/// ```rust
412/// use crate::{Tree, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
413/// use orbital_base_components::SignalModel;
414/// use std::collections::HashSet;
415/// let open = RwSignal::new(HashSet::new());
416/// view! {
417/// <div data-testid="tree-keyboard">
418/// <Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
419/// <TreeItem config=TreeItemConfig::leaf("alpha")><TreeItemLayout slot>"Alpha"</TreeItemLayout></TreeItem>
420/// <TreeItem config=TreeItemConfig::leaf("beta")><TreeItemLayout slot>"Beta"</TreeItemLayout></TreeItem>
421/// </Tree>
422/// </div>
423/// }
424/// ```
425///
426/// ## Inline editing
427/// Double-click or Enter on a focused row edits the label.
428/// <!-- preview -->
429/// ```rust
430/// use crate::{Tree, TreeBehavior, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
431/// use orbital_base_components::{Handler, SignalModel};
432/// use std::collections::HashSet;
433/// let open = RwSignal::new(HashSet::new());
434/// let log = RwSignal::new(String::new());
435/// let behavior = TreeBehavior::new()
436/// .with_editable(Signal::from(true))
437/// .with_on_label_change(Handler::on(move |(id, label)| log.set(format!("{id}:{label}"))));
438/// view! {
439/// <div data-testid="tree-editing">
440/// <Tree behavior=behavior expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
441/// <TreeItem config=TreeItemConfig::leaf("edit")><TreeItemLayout slot>"Rename me"</TreeItemLayout></TreeItem>
442/// </Tree>
443/// <span data-testid="tree-edit-log">{move || log.get()}</span>
444/// </div>
445/// }
446/// ```
447///
448/// ## Drag reorder
449/// Pointer drag reorders siblings when `reorderable` is enabled.
450/// <!-- preview -->
451/// ```rust
452/// use crate::{Tree, TreeBehavior, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
453/// use orbital_base_components::SignalModel;
454/// use std::collections::HashSet;
455/// let open = RwSignal::new(HashSet::new());
456/// let behavior = TreeBehavior::new().with_reorderable(Signal::from(true));
457/// view! {
458/// <div data-testid="tree-reorder">
459/// <Tree behavior=behavior expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
460/// <TreeItem config=TreeItemConfig::leaf("one")><TreeItemLayout slot>"One"</TreeItemLayout></TreeItem>
461/// <TreeItem config=TreeItemConfig::leaf("two")><TreeItemLayout slot>"Two"</TreeItemLayout></TreeItem>
462/// </Tree>
463/// </div>
464/// }
465/// ```
466///
467/// ## Custom collapse motion
468/// Override the CSS transition name via [`TreeItemCollapse`].
469/// <!-- preview -->
470/// ```rust
471/// use crate::{Tree, TreeExpansion, TreeItem, TreeItemCollapse, TreeItemConfig, TreeItemLayout, TreeSelection};
472/// use orbital_base_components::SignalModel;
473/// use std::collections::HashSet;
474/// let open = RwSignal::new(HashSet::new());
475/// view! {
476/// <div data-testid="tree-custom-motion">
477/// <Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
478/// <TreeItem config=TreeItemConfig::branch("motion")>
479/// <TreeItemLayout slot>
480/// "Animated branch"
481/// <TreeItemCollapse slot motion_name="orbital-tree-custom-collapse".to_string() />
482/// </TreeItemLayout>
483/// <TreeItem config=TreeItemConfig::leaf("child")><TreeItemLayout slot>"Child"</TreeItemLayout></TreeItem>
484/// </TreeItem>
485/// </Tree>
486/// </div>
487/// }
488/// ```
489#[component_doc(
490 category = "Tree Views",
491 preview_slug = "tree-view",
492 preview_label = "Tree View",
493 preview_icon = icondata::AiApartmentOutlined,
494)]
495#[component]
496pub fn Tree(
497 /// Expansion state and triggers.
498 #[prop(optional, into)]
499 expansion: TreeExpansion,
500 /// Selection mode and selected item ids.
501 #[prop(optional, into)]
502 selection: TreeSelection,
503 /// Editing, reorder, and interaction callbacks.
504 #[prop(optional, into)]
505 behavior: TreeBehavior,
506 /// Visual density and motion settings.
507 #[prop(optional, into)]
508 appearance: TreeAppearance,
509 /// Imperative tree API handle.
510 #[prop(default = TreeApiRef::default())]
511 api_ref: TreeApiRef,
512 /// Extra CSS class names merged onto the tree root.
513 #[prop(optional, into)]
514 class: MaybeProp<String>,
515 /// [`TreeItem`] children forming the expandable hierarchy.
516 children: Children,
517) -> impl IntoView {
518 inject_style("orbital-tree", tree_styles());
519
520 let tree_state = StoredValue::new(build_tree_state(
521 expansion,
522 selection,
523 behavior,
524 appearance.clone(),
525 ));
526 let state = tree_state.get_value();
527 let drag_listeners = install_tree_reorder_listeners(state.clone());
528 on_cleanup(move || drag_listeners.remove());
529 api_ref.load(TreeApiHandle::from_state(state.clone()));
530
531 let connectors = appearance.connectors;
532 let merged_class = Signal::derive(move || {
533 let mut parts = Vec::new();
534 if connectors.get() {
535 parts.push("orbital-tree--connectors".to_string());
536 }
537 if let Some(extra) = class.get() {
538 if !extra.is_empty() {
539 parts.push(extra);
540 }
541 }
542 parts.join(" ")
543 });
544
545 view! {
546 <BaseTreeRoot class=merged_class tree_state=state>{children()}</BaseTreeRoot>
547 }
548}