Skip to main content

dioxus_bootstrap_css/
dropdown.rs

1use dioxus::prelude::*;
2
3/// Bootstrap Dropdown component — signal-driven, no JavaScript.
4///
5/// ```rust
6/// let open = use_signal(|| false);
7/// rsx! {
8///     Dropdown { open: open,
9///         toggle: rsx! { "Actions" },
10///         menu: rsx! {
11///             DropdownItem { onclick: handler1, "Edit" }
12///             DropdownItem { onclick: handler2, "Delete" }
13///             DropdownDivider {}
14///             DropdownItem { disabled: true, "Archived" }
15///         },
16///     }
17/// }
18/// ```
19#[derive(Clone, PartialEq, Props)]
20pub struct DropdownProps {
21    /// Signal controlling dropdown open state.
22    pub open: Signal<bool>,
23    /// Toggle button content.
24    pub toggle: Element,
25    /// Dropdown menu content (DropdownItem components).
26    pub menu: Element,
27    /// Additional CSS classes for the dropdown container.
28    #[props(default)]
29    pub class: String,
30    /// Additional CSS classes for the toggle button.
31    #[props(default)]
32    pub toggle_class: String,
33    /// Drop direction.
34    #[props(default)]
35    pub direction: DropDirection,
36    /// Align menu to the end (right).
37    #[props(default)]
38    pub align_end: bool,
39}
40
41/// Dropdown direction.
42#[derive(Clone, Copy, Debug, Default, PartialEq)]
43pub enum DropDirection {
44    #[default]
45    Down,
46    Up,
47    Start,
48    End,
49}
50
51#[component]
52pub fn Dropdown(props: DropdownProps) -> Element {
53    let is_open = *props.open.read();
54    let mut open_signal = props.open;
55
56    let dir_class = match props.direction {
57        DropDirection::Down => "dropdown",
58        DropDirection::Up => "dropup",
59        DropDirection::Start => "dropstart",
60        DropDirection::End => "dropend",
61    };
62
63    let container_class = if props.class.is_empty() {
64        dir_class.to_string()
65    } else {
66        format!("{dir_class} {}", props.class)
67    };
68
69    let toggle_class = if props.toggle_class.is_empty() {
70        "btn btn-secondary dropdown-toggle".to_string()
71    } else {
72        format!("btn dropdown-toggle {}", props.toggle_class)
73    };
74
75    let menu_class = if is_open {
76        if props.align_end {
77            "dropdown-menu dropdown-menu-end show"
78        } else {
79            "dropdown-menu show"
80        }
81    } else if props.align_end {
82        "dropdown-menu dropdown-menu-end"
83    } else {
84        "dropdown-menu"
85    };
86
87    rsx! {
88        // Invisible overlay to close on outside click (only when open)
89        if is_open {
90            div {
91                style: "position: fixed; inset: 0; z-index: 990;",
92                onclick: move |_| open_signal.set(false),
93            }
94        }
95        div { class: "{container_class}",
96            style: if is_open { "position: relative; z-index: 991;" } else { "" },
97            button {
98                class: "{toggle_class}",
99                r#type: "button",
100                "aria-expanded": if is_open { "true" } else { "false" },
101                onclick: move |evt| {
102                    evt.stop_propagation();
103                    open_signal.set(!is_open);
104                },
105                {props.toggle}
106            }
107            ul { class: "{menu_class}",
108                // Close dropdown when clicking an item
109                onclick: move |_| open_signal.set(false),
110                {props.menu}
111            }
112        }
113    }
114}
115
116/// A single item in a Dropdown menu.
117#[derive(Clone, PartialEq, Props)]
118pub struct DropdownItemProps {
119    /// Active state.
120    #[props(default)]
121    pub active: bool,
122    /// Disabled state.
123    #[props(default)]
124    pub disabled: bool,
125    /// Click event handler.
126    #[props(default)]
127    pub onclick: Option<EventHandler<MouseEvent>>,
128    /// Additional CSS classes.
129    #[props(default)]
130    pub class: String,
131    /// Child elements.
132    pub children: Element,
133}
134
135#[component]
136pub fn DropdownItem(props: DropdownItemProps) -> Element {
137    let mut classes = vec!["dropdown-item".to_string()];
138    if props.active {
139        classes.push("active".to_string());
140    }
141    if props.disabled {
142        classes.push("disabled".to_string());
143    }
144    if !props.class.is_empty() {
145        classes.push(props.class.clone());
146    }
147    let full_class = classes.join(" ");
148
149    rsx! {
150        li {
151            button {
152                class: "{full_class}",
153                r#type: "button",
154                disabled: props.disabled,
155                onclick: move |evt| {
156                    if let Some(handler) = &props.onclick {
157                        handler.call(evt);
158                    }
159                },
160                {props.children}
161            }
162        }
163    }
164}
165
166/// Dropdown menu divider.
167#[component]
168pub fn DropdownDivider() -> Element {
169    rsx! {
170        li { hr { class: "dropdown-divider" } }
171    }
172}
173
174/// Dropdown menu header text.
175#[derive(Clone, PartialEq, Props)]
176pub struct DropdownHeaderProps {
177    pub children: Element,
178}
179
180#[component]
181pub fn DropdownHeader(props: DropdownHeaderProps) -> Element {
182    rsx! {
183        li { h6 { class: "dropdown-header", {props.children} } }
184    }
185}