Skip to main content

orbital_base_components/form/listbox/
mod.rs

1mod active_descendant;
2mod option_walker;
3mod utils;
4
5use leptos::{context::Provider, ev, html, prelude::*};
6use std::sync::Arc;
7use web_sys::{HtmlElement, Node};
8
9pub use active_descendant::{use_active_descendant, ActiveDescendantController};
10pub use utils::{get_dropdown_action_from_key, DropdownAction};
11
12/// Headless listbox wrapper used by Combobox and AutoComplete.
13#[component]
14pub fn BaseListbox(
15    #[prop(optional, into)] class: MaybeProp<String>,
16    set_listbox: Arc<dyn Fn(Node) + Send + Sync>,
17    listbox_ref: NodeRef<html::Div>,
18    children: Children,
19) -> impl IntoView {
20    let trigger = ArcTrigger::new();
21    let effect = RenderEffect::new({
22        let trigger = trigger.clone();
23        move |_| {
24            trigger.track();
25            if let Some(listbox_el) = listbox_ref.get() {
26                set_listbox(listbox_el.into());
27            }
28        }
29    });
30
31    on_cleanup(move || {
32        drop(effect);
33    });
34
35    view! {
36        <div
37            class=move || {
38                let mut parts = vec!["orbital-listbox".to_string()];
39                if let Some(extra) = class.get() {
40                    if !extra.is_empty() {
41                        parts.push(extra);
42                    }
43                }
44                parts.join(" ")
45            }
46            node_ref=listbox_ref
47            role="listbox"
48            on:mousedown=|e| e.prevent_default()
49        >
50            <Provider value=ListboxInjection(trigger)>{children()}</Provider>
51        </div>
52    }
53}
54
55/// Shared option lifecycle trigger for listbox option registration.
56#[derive(Clone)]
57pub struct ListboxInjection(ArcTrigger);
58
59impl ListboxInjection {
60    #[inline]
61    pub fn expect_context() -> Self {
62        expect_context()
63    }
64
65    #[inline]
66    pub fn trigger(&self) {
67        self.0.notify();
68    }
69}
70
71/// Standard listbox keyboard navigation behavior.
72pub fn listbox_keyboard_event(
73    e: ev::KeyboardEvent,
74    open: RwSignal<bool>,
75    multiselect: bool,
76    active_descendant_controller: &ActiveDescendantController,
77    select_option: impl Fn(HtmlElement),
78) {
79    let (open, set_open) = open.split();
80    let open = open.get_untracked();
81    let action = get_dropdown_action_from_key(&e, open, multiselect);
82    let active_option = active_descendant_controller.active();
83
84    match action {
85        DropdownAction::Type | DropdownAction::Open => {
86            if !open {
87                set_open.set(true);
88            }
89            if action == DropdownAction::Open {
90                e.prevent_default();
91            }
92        }
93        DropdownAction::CloseSelect | DropdownAction::Select => {
94            if let Some(option) = active_option {
95                e.prevent_default();
96                select_option(option);
97            }
98        }
99        DropdownAction::Next => {
100            e.prevent_default();
101            if active_option.is_some() {
102                active_descendant_controller.next();
103            } else {
104                active_descendant_controller.first();
105            }
106        }
107        DropdownAction::Previous => {
108            e.prevent_default();
109            if active_option.is_some() {
110                active_descendant_controller.prev();
111            } else {
112                active_descendant_controller.first();
113            }
114        }
115        DropdownAction::First | DropdownAction::PageUp => {
116            e.prevent_default();
117            active_descendant_controller.first();
118        }
119        DropdownAction::Last | DropdownAction::PageDown => {
120            e.prevent_default();
121            active_descendant_controller.last();
122        }
123        DropdownAction::Tab | DropdownAction::Close | DropdownAction::None => {}
124    };
125}