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        div { class: "{container_class}",
89            button {
90                class: "{toggle_class}",
91                r#type: "button",
92                "aria-expanded": if is_open { "true" } else { "false" },
93                onclick: move |_| open_signal.set(!is_open),
94                {props.toggle}
95            }
96            ul { class: "{menu_class}",
97                // Close dropdown when clicking an item
98                onclick: move |_| open_signal.set(false),
99                {props.menu}
100            }
101        }
102    }
103}
104
105/// A single item in a Dropdown menu.
106#[derive(Clone, PartialEq, Props)]
107pub struct DropdownItemProps {
108    /// Active state.
109    #[props(default)]
110    pub active: bool,
111    /// Disabled state.
112    #[props(default)]
113    pub disabled: bool,
114    /// Click event handler.
115    #[props(default)]
116    pub onclick: Option<EventHandler<MouseEvent>>,
117    /// Additional CSS classes.
118    #[props(default)]
119    pub class: String,
120    /// Child elements.
121    pub children: Element,
122}
123
124#[component]
125pub fn DropdownItem(props: DropdownItemProps) -> Element {
126    let mut classes = vec!["dropdown-item".to_string()];
127    if props.active {
128        classes.push("active".to_string());
129    }
130    if props.disabled {
131        classes.push("disabled".to_string());
132    }
133    if !props.class.is_empty() {
134        classes.push(props.class.clone());
135    }
136    let full_class = classes.join(" ");
137
138    rsx! {
139        li {
140            button {
141                class: "{full_class}",
142                r#type: "button",
143                disabled: props.disabled,
144                onclick: move |evt| {
145                    if let Some(handler) = &props.onclick {
146                        handler.call(evt);
147                    }
148                },
149                {props.children}
150            }
151        }
152    }
153}
154
155/// Dropdown menu divider.
156#[component]
157pub fn DropdownDivider() -> Element {
158    rsx! {
159        li { hr { class: "dropdown-divider" } }
160    }
161}
162
163/// Dropdown menu header text.
164#[derive(Clone, PartialEq, Props)]
165pub struct DropdownHeaderProps {
166    pub children: Element,
167}
168
169#[component]
170pub fn DropdownHeader(props: DropdownHeaderProps) -> Element {
171    rsx! {
172        li { h6 { class: "dropdown-header", {props.children} } }
173    }
174}