Skip to main content

dioxus_bootstrap/
button.rs

1use dioxus::prelude::*;
2use super::size::*;
3use dioxus_router::navigation::NavigationTarget;
4use dioxus_router::components::Link;
5
6#[derive(Clone, Copy, PartialEq)]
7pub enum ButtonVariant {
8    Basic,
9    Primary,
10    Secondary,
11    Success,
12    Danger,
13    Warning,
14    Info,
15    Light,
16    Dark,
17    Link,
18}
19
20impl Into<&'static str> for ButtonVariant {
21    fn into(self) -> &'static str {
22        match self {
23            ButtonVariant::Primary => "primary",
24            ButtonVariant::Secondary => "secondary",
25            ButtonVariant::Success => "success",
26            ButtonVariant::Danger => "danger",
27            ButtonVariant::Warning => "warning",
28            ButtonVariant::Info => "info",
29            ButtonVariant::Light => "light",
30            ButtonVariant::Dark => "dark",
31            ButtonVariant::Link => "link",
32            _ => "",
33        }
34    }
35}
36
37#[derive(Clone, Copy, Default, PartialEq)]
38pub enum ButtonType {
39    #[default]
40    Button,
41    Reset,
42    Submit,
43}
44
45impl Into<&'static str> for ButtonType {
46    fn into(self) -> &'static str {
47        match self {
48            ButtonType::Button => "button",
49            ButtonType::Reset => "reset",
50            ButtonType::Submit => "submit",
51        }
52    }
53}
54
55#[derive(Clone, Props, PartialEq)]
56pub struct ButtonProps {
57    #[props(optional)]
58    id: String,
59    #[props(optional, default = ButtonVariant::Basic)]
60    variant: ButtonVariant,
61    #[props(optional, default = Size::Normal)]
62    size: Size,
63    #[props(optional, default = false)]
64    disabled: bool,
65    #[props(optional, default = false)]
66    outline: bool,
67    #[props(optional, default = false)]
68    nowrap: bool,
69    #[props(optional, default = false)]
70    toggle: bool,
71    /// active controls whether a toggle button is on or off.
72    #[props(optional, default = false)]
73    active: bool,
74    #[props(optional, default = "".to_string())]
75    style: String,
76    #[props(optional, default = ButtonType::Button)]
77    button_type: ButtonType,
78    
79    /// Additional CSS classes
80    #[props(optional, default = "".to_string())]
81    class: String,
82    
83    /// Button text content (alternative to children)
84    #[props(optional, default = None)]
85    text: Option<String>,
86    
87    /// Loading state
88    #[props(optional, default = false)]
89    loading: bool,
90    
91    /// Close button variant
92    #[props(optional, default = false)]
93    close: bool,
94    
95    /// Floating action button
96    #[props(optional, default = false)]
97    floating: bool,
98
99    /// If present, generate the button as an 'a' tag using the dioxus router.
100    #[props(optional, default = None)]
101    link_to: Option<NavigationTarget>,
102
103    children: Element,
104    #[props(optional)]
105    onclick: EventHandler<MouseEvent>,
106    #[props(optional)]
107    onmounted: EventHandler<MountedEvent>,
108}
109
110#[component]
111pub fn Button(props: ButtonProps) -> Element {
112    let mut class_list = vec!["btn".to_string()];
113
114    if props.disabled { class_list.push("btn-disabled".into()) }
115    if props.toggle && props.active { class_list.push("active".into()) }
116
117    let variant: &str = props.variant.into();
118    if variant.len() > 0 {
119        if props.outline {
120            class_list.push(format!("btn-outline-{}", variant))
121        } else {
122            class_list.push(format!("btn-{}", variant))
123        }
124    }
125
126    let size: &str = props.size.into();
127    if props.size != Size::Normal {
128        class_list.push(format!("btn-{}", size));
129    }
130
131    // Add additional classes
132    if !props.class.is_empty() {
133        class_list.push(props.class.clone());
134    }
135    
136    if props.loading {
137        class_list.push("btn-loading".to_string());
138    }
139    
140    if props.floating {
141        class_list.push("btn-floating".to_string());
142    }
143    
144    let class_list = class_list.join(" ");
145
146    if props.toggle {
147        return rsx! {
148            button { id: props.id, r#type: "button", style: props.style, onclick: props.onclick, class: class_list, "data-bs-toggle": "button", "aria-pressed": true, onmounted: props.onmounted, {props.children} }
149        }
150    }
151    
152    // Handle close button
153    if props.close {
154        return rsx! {
155            button {
156                id: props.id,
157                r#type: "button",
158                class: "btn-close",
159                style: props.style,
160                onclick: props.onclick,
161                disabled: props.disabled,
162                "aria-label": "Close",
163                onmounted: props.onmounted,
164            }
165        };
166    }
167    
168    let button_type_str: &str = props.button_type.into();
169    
170    match props.button_type {
171        ButtonType::Submit | ButtonType::Reset => rsx!{
172            input { 
173                id: props.id, 
174                r#type: button_type_str, 
175                value: if props.button_type == ButtonType::Submit { "Submit" } else { "Reset" }, 
176                style: props.style, 
177                onclick: props.onclick, 
178                class: class_list, 
179                disabled: props.disabled, 
180                onmounted: props.onmounted 
181            }
182        },
183        _ => match props.link_to {
184            Some(t) => rsx!{
185                Link {
186                    to: t,
187                    id: props.id,
188                    onclick: props.onclick,
189                    style: props.style,
190                    class: class_list,
191                    "aria-disabled": props.disabled,
192                    {if let Some(text) = props.text { rsx! { "{text}" } } else { props.children }}
193                }
194            },
195            _ => rsx! {
196                button { 
197                    id: props.id, 
198                    r#type: button_type_str, 
199                    style: props.style, 
200                    onclick: props.onclick, 
201                    class: class_list, 
202                    disabled: props.disabled, 
203                    onmounted: props.onmounted, 
204                    {if let Some(text) = props.text { rsx! { "{text}" } } else { props.children }}
205                }
206            }
207        }
208    }
209
210}
211
212#[derive(Clone, Copy, PartialEq)]
213pub enum ButtonGroupOrientation {
214    Horizontal,
215    Vertical,
216}
217
218#[derive(Clone, Props, PartialEq)]
219pub struct ButtonGroupProps {
220    /// The label generates an aria-label attribute for screen readers.
221    #[props(optional, default = "Button group".to_string())]
222    label: String,
223    #[props(optional, default = Size::Normal)]
224    size: Size,
225    #[props(optional, default = ButtonGroupOrientation::Horizontal)]
226    orientation: ButtonGroupOrientation,
227    #[props(optional, default = false)]
228    toolbar: bool,
229    children: Element,
230}
231
232#[component]
233pub fn ButtonGroup(props: ButtonGroupProps) -> Element {
234    let mut class_list = vec!["btn-group".to_string()];
235    
236    if props.orientation == ButtonGroupOrientation::Vertical {
237        class_list = vec!["btn-group-vertical".to_string()];
238    }
239    
240    let size: &str = props.size.into();
241    if props.size != Size::Normal {
242        class_list.push(format!("btn-group-{}", size));
243    }
244    
245    if props.toolbar {
246        class_list = vec!["btn-toolbar".to_string()];
247    }
248
249    let class_list = class_list.join(" ");
250    let role = if props.toolbar { "toolbar" } else { "group" };
251    
252    rsx! {
253        div {
254            class: class_list,
255            role: role,
256            "aria-label": props.label,
257            {props.children}
258        }
259    }
260}
261
262#[derive(Clone, Props, PartialEq)]
263pub struct DropdownButtonProps {
264    #[props(optional)]
265    id: String,
266    #[props(optional, default = "".to_string())]
267    class: String,
268    #[props(optional, default = ButtonVariant::Primary)]
269    variant: ButtonVariant,
270    #[props(optional, default = Size::Normal)]
271    size: Size,
272    #[props(optional, default = false)]
273    disabled: bool,
274    #[props(optional, default = false)]
275    outline: bool,
276    #[props(optional, default = false)]
277    split: bool,
278    #[props(optional, default = "Dropdown".to_string())]
279    text: String,
280    children: Element,
281}
282
283#[component]
284pub fn DropdownButton(props: DropdownButtonProps) -> Element {
285    let mut class_list = vec!["btn".to_string(), "dropdown-toggle".to_string()];
286    
287    let variant: &str = props.variant.into();
288    if !variant.is_empty() {
289        if props.outline {
290            class_list.push(format!("btn-outline-{}", variant));
291        } else {
292            class_list.push(format!("btn-{}", variant));
293        }
294    }
295    
296    let size: &str = props.size.into();
297    if props.size != Size::Normal {
298        class_list.push(format!("btn-{}", size));
299    }
300    
301    // Add additional classes
302    if !props.class.is_empty() {
303        class_list.push(props.class.clone());
304    }
305    
306    let class_list = class_list.join(" ");
307    
308    if props.split {
309        rsx! {
310            div {
311                class: "btn-group",
312                button {
313                    r#type: "button",
314                    class: class_list.replace("dropdown-toggle", "").trim(),
315                    disabled: props.disabled,
316                    "{props.text}"
317                }
318                button {
319                    r#type: "button",
320                    class: format!("{} dropdown-toggle dropdown-toggle-split", class_list.replace("dropdown-toggle", "").trim()),
321                    "data-bs-toggle": "dropdown",
322                    "aria-expanded": "false",
323                    disabled: props.disabled,
324                    span {
325                        class: "visually-hidden",
326                        "Toggle Dropdown"
327                    }
328                }
329                ul {
330                    class: "dropdown-menu",
331                    {props.children}
332                }
333            }
334        }
335    } else {
336        rsx! {
337            div {
338                class: "dropdown",
339                button {
340                    id: props.id,
341                    r#type: "button",
342                    class: class_list,
343                    "data-bs-toggle": "dropdown",
344                    "aria-expanded": "false",
345                    disabled: props.disabled,
346                    "{props.text}"
347                }
348                ul {
349                    class: "dropdown-menu",
350                    {props.children}
351                }
352            }
353        }
354    }
355}