dioxus_tw_components/components/
dropdown.rs1use 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#[component]
97pub fn Dropdown(mut props: DropdownProps) -> Element {
98 let parent_state = try_use_context::<Signal<DropdownState>>();
101 let mut state = use_context_provider(|| Signal::new(DropdownState::new()));
102
103 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 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}