Skip to main content

dioxus_tw_components/components/
dropdown.rs

1use crate::dioxus_core::IntoAttributeValue;
2use dioxus::prelude::*;
3use dioxus_core::AttributeValue;
4
5#[derive(Clone, Copy)]
6pub struct DropdownManager {
7    generation: u64,
8}
9
10impl DropdownManager {
11    pub(crate) fn new() -> Self {
12        Self { generation: 0 }
13    }
14
15    pub fn advance(&mut self) -> u64 {
16        self.generation += 1;
17        self.generation
18    }
19
20    pub(crate) fn generation(&self) -> u64 {
21        self.generation
22    }
23}
24
25#[derive(Clone, PartialEq, Props)]
26pub struct DropdownManagerProviderProps {
27    children: Element,
28}
29
30#[component]
31pub fn DropdownManagerProvider(props: DropdownManagerProviderProps) -> Element {
32    use_context_provider(|| Signal::new(DropdownManager::new()));
33    rsx! { {props.children} }
34}
35
36#[derive(Clone, Copy)]
37struct DropdownState {
38    is_active: bool,
39    manager_generation: u64,
40}
41
42impl DropdownState {
43    fn new() -> Self {
44        Self {
45            is_active: false,
46            manager_generation: 0,
47        }
48    }
49
50    fn open_with_generation(&mut self, generation: u64) {
51        self.is_active = true;
52        self.manager_generation = generation;
53    }
54
55    fn toggle(&mut self) {
56        self.is_active = !self.is_active;
57    }
58
59    fn close(&mut self) {
60        self.is_active = false;
61    }
62
63    fn get_is_active(&self) -> bool {
64        self.is_active
65    }
66}
67
68impl IntoAttributeValue for DropdownState {
69    fn into_value(self) -> AttributeValue {
70        match self.is_active {
71            true => AttributeValue::Text("open".to_string()),
72            false => AttributeValue::Text("closed".to_string()),
73        }
74    }
75}
76
77#[derive(Clone, PartialEq, Props)]
78pub struct DropdownProps {
79    #[props(extends = div, extends = GlobalAttributes)]
80    attributes: Vec<Attribute>,
81    children: Element,
82}
83
84/// Usage:
85/// ```ignore
86/// Dropdown {
87///    DropdownToggle {
88///        "Dropdown"
89///     }
90///     DropdownContent {
91///       div { "content" }
92///    }
93/// }
94/// ```
95/// Use 0 closing_delay_ms to disable the auto close feature
96#[component]
97pub fn Dropdown(mut props: DropdownProps) -> Element {
98    // Capture parent dropdown state before providing our own,
99    // so clicking backdrop closes the entire dropdown chain.
100    let parent_state = try_use_context::<Signal<DropdownState>>();
101    let mut state = use_context_provider(|| Signal::new(DropdownState::new()));
102
103    // Generation counter: close this dropdown when another interaction advances the counter
104    let manager = try_use_context::<Signal<DropdownManager>>();
105
106    use_effect(move || {
107        if let Some(mgr) = manager {
108            let global_gen = mgr.read().generation();
109            if state.peek().get_is_active() && global_gen > state.peek().manager_generation {
110                state.write().close();
111            }
112        }
113    });
114
115    let default_classes = "dropdown";
116    crate::setup_class_attribute(&mut props.attributes, default_classes);
117
118    rsx! {
119        div { "data-state": state.read().into_value(), ..props.attributes, {props.children} }
120        if state.read().get_is_active() {
121            div {
122                class: "dropdown-backdrop",
123                onclick: move |_event| {
124                    state.write().close();
125                    if let Some(mut parent) = parent_state {
126                        parent.write().close();
127                    }
128                    // Advance generation so sibling dropdowns close too
129                    if let Some(mut mgr) = manager {
130                        mgr.write().advance();
131                    }
132                },
133            }
134        }
135    }
136}
137
138#[derive(Clone, PartialEq, Props)]
139pub struct DropdownToggleProps {
140    #[props(extends = button, extends = GlobalAttributes)]
141    attributes: Vec<Attribute>,
142
143    children: Element,
144}
145
146#[component]
147pub fn DropdownToggle(mut props: DropdownToggleProps) -> Element {
148    let mut state = use_context::<Signal<DropdownState>>();
149    let manager = try_use_context::<Signal<DropdownManager>>();
150
151    let default_classes = "button";
152    crate::setup_class_attribute(&mut props.attributes, default_classes);
153
154    rsx! {
155        button {
156            onclick: move |e: MouseEvent| {
157                e.stop_propagation();
158                e.prevent_default();
159
160                let will_open = !state.read().get_is_active();
161                if will_open {
162                    if let Some(mut mgr) = manager {
163                        let new_gen = mgr.write().advance();
164                        state.write().open_with_generation(new_gen);
165                    } else {
166                        state.write().toggle();
167                    }
168                } else {
169                    state.write().toggle();
170                }
171            },
172            ..props.attributes,
173            {props.children}
174        }
175    }
176}
177
178#[derive(Clone, PartialEq, Props)]
179pub struct DropdownContentProps {
180    #[props(extends = div, extends = GlobalAttributes)]
181    attributes: Vec<Attribute>,
182
183    children: Element,
184}
185
186#[component]
187pub fn DropdownContent(mut props: DropdownContentProps) -> Element {
188    let mut state = use_context::<Signal<DropdownState>>();
189
190    let default_classes = "dropdown-content";
191    crate::setup_class_attribute(&mut props.attributes, default_classes);
192
193    rsx! {
194        div {
195            "data-state": state.read().into_value(),
196            onclick: move |_| {
197                state.write().close();
198            },
199            ..props.attributes,
200            {props.children}
201        }
202    }
203}