1use crate::components::config_provider::{ComponentSize, use_config};
2use crate::components::floating::use_floating_close_handle;
3use crate::components::overlay::OverlayKind;
4use crate::components::select_base::use_floating_layer;
5use dioxus::events::{KeyboardEvent, MouseEvent};
6use dioxus::prelude::Key;
7use dioxus::prelude::*;
8
9#[derive(Clone, Debug, PartialEq)]
11pub struct DropdownItem {
12 pub key: String,
13 pub label: String,
14 pub disabled: bool,
15}
16
17impl DropdownItem {
18 pub fn new(key: impl Into<String>, label: impl Into<String>) -> Self {
19 Self {
20 key: key.into(),
21 label: label.into(),
22 disabled: false,
23 }
24 }
25}
26
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
29pub enum DropdownTrigger {
30 #[default]
31 Click,
32 Hover,
33}
34
35#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
37pub enum DropdownPlacement {
38 #[default]
39 BottomLeft,
40 BottomRight,
41}
42
43#[derive(Props, Clone, PartialEq)]
45pub struct DropdownProps {
46 pub items: Vec<DropdownItem>,
48 #[props(default)]
50 pub trigger: DropdownTrigger,
51 #[props(optional)]
53 pub placement: Option<DropdownPlacement>,
54 #[props(optional)]
56 pub open: Option<bool>,
57 #[props(optional)]
59 pub default_open: Option<bool>,
60 #[props(optional)]
62 pub on_open_change: Option<EventHandler<bool>>,
63 #[props(optional)]
65 pub on_click: Option<EventHandler<String>>,
66 #[props(default)]
68 pub disabled: bool,
69 #[props(optional)]
71 pub class: Option<String>,
72 #[props(optional)]
74 pub overlay_class: Option<String>,
75 #[props(optional)]
77 pub overlay_style: Option<String>,
78 #[props(optional)]
80 pub overlay_width: Option<f32>,
81 pub children: Element,
83}
84
85#[component]
87pub fn Dropdown(props: DropdownProps) -> Element {
88 let DropdownProps {
89 items,
90 trigger,
91 placement,
92 open,
93 default_open,
94 on_open_change,
95 on_click,
96 disabled,
97 class,
98 overlay_class,
99 overlay_style,
100 overlay_width,
101 children,
102 } = props;
103
104 let config = use_config();
105 let global_size = config.size;
106
107 let open_state: Signal<bool> = use_signal(|| default_open.unwrap_or(false));
108 let is_controlled = open.is_some();
109 let current_open = open.unwrap_or(*open_state.read());
110
111 let floating = use_floating_layer(OverlayKind::Dropdown, current_open);
112 let current_z = *floating.z_index.read();
113
114 let close_handle = if !is_controlled && matches!(trigger, DropdownTrigger::Click) {
115 Some(use_floating_close_handle(open_state))
116 } else {
117 None
118 };
119
120 let disabled_flag = disabled;
121 let is_controlled_flag = is_controlled;
122 let open_for_handlers = open_state;
123 let on_open_change_cb = on_open_change;
124 let trigger_mode = trigger;
125 let current_open_flag = current_open;
126 let close_handle_for_click = close_handle;
127 let close_handle_for_menu = close_handle;
128
129 let class_attr = {
130 let mut list = vec!["adui-dropdown-root".to_string()];
131 if let Some(extra) = class {
132 list.push(extra);
133 }
134 list.join(" ")
135 };
136
137 let overlay_class_attr = {
138 let mut list = vec!["adui-dropdown-menu".to_string()];
139 if let Some(extra) = overlay_class {
140 list.push(extra);
141 }
142 list.join(" ")
143 };
144
145 let placement = placement.unwrap_or_default();
146 let width_style = overlay_width
147 .map(|w| format!("min-width: {w}px;"))
148 .unwrap_or_default();
149
150 let align_style = match placement {
151 DropdownPlacement::BottomLeft => "left: 0;",
152 DropdownPlacement::BottomRight => "right: 0;",
153 };
154
155 let overlay_style_attr = {
156 let extra = overlay_style.unwrap_or_default();
157 format!(
158 "position: absolute; top: 100%; margin-top: 4px; z-index: {}; {}; {} {}",
159 current_z, align_style, width_style, extra
160 )
161 };
162
163 let size_class = match global_size {
164 ComponentSize::Small => "adui-dropdown-sm",
165 ComponentSize::Large => "adui-dropdown-lg",
166 ComponentSize::Middle => "adui-dropdown-md",
167 };
168
169 let on_click_cb = on_click;
170
171 rsx! {
172 span {
173 class: "{class_attr}",
174 style: "position: relative; display: inline-block;",
175 onmouseenter: move |_evt: MouseEvent| {
176 if matches!(trigger_mode, DropdownTrigger::Hover) {
177 crate::components::tooltip::update_open_state(
178 disabled_flag,
179 is_controlled_flag,
180 open_for_handlers,
181 on_open_change_cb,
182 true,
183 );
184 }
185 },
186 onmouseleave: move |_evt: MouseEvent| {
187 if matches!(trigger_mode, DropdownTrigger::Hover) {
188 crate::components::tooltip::update_open_state(
189 disabled_flag,
190 is_controlled_flag,
191 open_for_handlers,
192 on_open_change_cb,
193 false,
194 );
195 }
196 },
197 onclick: move |_evt: MouseEvent| {
198 if !matches!(trigger_mode, DropdownTrigger::Click) {
199 return;
200 }
201 if let Some(handle) = close_handle_for_click {
202 handle.mark_internal_click();
203 }
204 crate::components::tooltip::update_open_state(
205 disabled_flag,
206 is_controlled_flag,
207 open_for_handlers,
208 on_open_change_cb,
209 !current_open_flag,
210 );
211 },
212 onkeydown: move |evt: KeyboardEvent| {
213 if matches!(evt.key(), Key::Escape) {
214 evt.prevent_default();
215 crate::components::tooltip::update_open_state(
216 disabled_flag,
217 is_controlled_flag,
218 open_for_handlers,
219 on_open_change_cb,
220 false,
221 );
222 }
223 },
224 {children}
225 if current_open {
226 div {
227 class: "{overlay_class_attr} {size_class}",
228 style: "{overlay_style_attr}",
229 onclick: move |_evt| {
230 if let Some(handle) = close_handle_for_menu {
231 handle.mark_internal_click();
232 }
233 },
234 ul {
235 class: "adui-dropdown-menu-list",
236 {items.iter().map(|item| {
237 let key = item.key.clone();
238 let label = item.label.clone();
239 let disabled_item = item.disabled || disabled_flag;
240 rsx! {
241 li {
242 class: {
243 let mut list = vec!["adui-dropdown-menu-item".to_string()];
244 if disabled_item {
245 list.push("adui-dropdown-menu-item-disabled".into());
246 }
247 list.join(" ")
248 },
249 onclick: move |_evt| {
250 if disabled_item {
251 return;
252 }
253 crate::components::tooltip::update_open_state(
254 disabled_flag,
255 is_controlled_flag,
256 open_for_handlers,
257 on_open_change_cb,
258 false,
259 );
260 if let Some(cb) = on_click_cb {
261 cb.call(key.clone());
262 }
263 },
264 "{label}"
265 }
266 }
267 })}
268 }
269 }
270 }
271 }
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn dropdown_item_new() {
281 let item = DropdownItem::new("key1", "Label 1");
282 assert_eq!(item.key, "key1");
283 assert_eq!(item.label, "Label 1");
284 assert_eq!(item.disabled, false);
285 }
286
287 #[test]
288 fn dropdown_item_new_with_strings() {
289 let item = DropdownItem::new(String::from("key2"), String::from("Label 2"));
290 assert_eq!(item.key, "key2");
291 assert_eq!(item.label, "Label 2");
292 assert_eq!(item.disabled, false);
293 }
294
295 #[test]
296 fn dropdown_item_clone() {
297 let item1 = DropdownItem::new("key1", "Label 1");
298 let item2 = item1.clone();
299 assert_eq!(item1, item2);
300 }
301
302 #[test]
303 fn dropdown_item_partial_eq() {
304 let item1 = DropdownItem::new("key1", "Label 1");
305 let item2 = DropdownItem::new("key1", "Label 1");
306 let item3 = DropdownItem::new("key2", "Label 2");
307 assert_eq!(item1, item2);
308 assert_ne!(item1, item3);
309 }
310
311 #[test]
312 fn dropdown_trigger_default() {
313 assert_eq!(DropdownTrigger::default(), DropdownTrigger::Click);
314 }
315
316 #[test]
317 fn dropdown_trigger_all_variants() {
318 assert_eq!(DropdownTrigger::Click, DropdownTrigger::Click);
319 assert_eq!(DropdownTrigger::Hover, DropdownTrigger::Hover);
320 assert_ne!(DropdownTrigger::Click, DropdownTrigger::Hover);
321 }
322
323 #[test]
324 fn dropdown_trigger_clone() {
325 let original = DropdownTrigger::Hover;
326 let cloned = original;
327 assert_eq!(original, cloned);
328 }
329
330 #[test]
331 fn dropdown_placement_default() {
332 assert_eq!(DropdownPlacement::default(), DropdownPlacement::BottomLeft);
333 }
334
335 #[test]
336 fn dropdown_placement_all_variants() {
337 assert_eq!(DropdownPlacement::BottomLeft, DropdownPlacement::BottomLeft);
338 assert_eq!(
339 DropdownPlacement::BottomRight,
340 DropdownPlacement::BottomRight
341 );
342 assert_ne!(
343 DropdownPlacement::BottomLeft,
344 DropdownPlacement::BottomRight
345 );
346 }
347
348 #[test]
349 fn dropdown_placement_clone() {
350 let original = DropdownPlacement::BottomRight;
351 let cloned = original;
352 assert_eq!(original, cloned);
353 }
354}