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    #[props(optional)]
104    children: Element,
105    #[props(optional)]
106    onclick: EventHandler<MouseEvent>,
107    #[props(optional)]
108    onmounted: EventHandler<MountedEvent>,
109}
110
111#[component]
112pub fn Button(props: ButtonProps) -> Element {
113    let mut class_list = vec!["btn".to_string()];
114
115    if props.disabled { class_list.push("btn-disabled".into()) }
116    if props.toggle && props.active { class_list.push("active".into()) }
117
118    let variant: &str = props.variant.into();
119    if variant.len() > 0 {
120        if props.outline {
121            class_list.push(format!("btn-outline-{}", variant))
122        } else {
123            class_list.push(format!("btn-{}", variant))
124        }
125    }
126
127    let size: &str = props.size.into();
128    if props.size != Size::Normal {
129        class_list.push(format!("btn-{}", size));
130    }
131
132    // Add additional classes
133    if !props.class.is_empty() {
134        class_list.push(props.class.clone());
135    }
136    
137    if props.loading {
138        class_list.push("btn-loading".to_string());
139    }
140    
141    if props.floating {
142        class_list.push("btn-floating".to_string());
143    }
144    
145    let class_list = class_list.join(" ");
146
147    if props.toggle {
148        return rsx! {
149            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} }
150        }
151    }
152    
153    // Handle close button
154    if props.close {
155        return rsx! {
156            button {
157                id: props.id,
158                r#type: "button",
159                class: "btn-close",
160                style: props.style,
161                onclick: props.onclick,
162                disabled: props.disabled,
163                "aria-label": "Close",
164                onmounted: props.onmounted,
165            }
166        };
167    }
168    
169    let button_type_str: &str = props.button_type.into();
170    
171    match props.button_type {
172        ButtonType::Submit | ButtonType::Reset => rsx!{
173            input { 
174                id: props.id, 
175                r#type: button_type_str, 
176                value: if props.button_type == ButtonType::Submit { "Submit" } else { "Reset" }, 
177                style: props.style, 
178                onclick: props.onclick, 
179                class: class_list, 
180                disabled: props.disabled, 
181                onmounted: props.onmounted 
182            }
183        },
184        _ => match props.link_to {
185            Some(t) => rsx!{
186                Link {
187                    to: t,
188                    id: props.id,
189                    onclick: props.onclick,
190                    style: props.style,
191                    class: class_list,
192                    "aria-disabled": props.disabled,
193                    {if let Some(text) = props.text { rsx! { "{text}" } } else { props.children }}
194                }
195            },
196            _ => rsx! {
197                button { 
198                    id: props.id, 
199                    r#type: button_type_str, 
200                    style: props.style, 
201                    onclick: props.onclick, 
202                    class: class_list, 
203                    disabled: props.disabled, 
204                    onmounted: props.onmounted, 
205                    {if let Some(text) = props.text { rsx! { "{text}" } } else { props.children }}
206                }
207            }
208        }
209    }
210
211}
212
213#[derive(Clone, Copy, PartialEq)]
214pub enum ButtonGroupOrientation {
215    Horizontal,
216    Vertical,
217}
218
219#[derive(Clone, Props, PartialEq)]
220pub struct ButtonGroupProps {
221    /// The label generates an aria-label attribute for screen readers.
222    #[props(optional, default = "Button group".to_string())]
223    label: String,
224    #[props(optional, default = Size::Normal)]
225    size: Size,
226    #[props(optional, default = ButtonGroupOrientation::Horizontal)]
227    orientation: ButtonGroupOrientation,
228    #[props(optional, default = false)]
229    toolbar: bool,
230    children: Element,
231}
232
233#[component]
234pub fn ButtonGroup(props: ButtonGroupProps) -> Element {
235    let mut class_list = vec!["btn-group".to_string()];
236    
237    if props.orientation == ButtonGroupOrientation::Vertical {
238        class_list = vec!["btn-group-vertical".to_string()];
239    }
240    
241    let size: &str = props.size.into();
242    if props.size != Size::Normal {
243        class_list.push(format!("btn-group-{}", size));
244    }
245    
246    if props.toolbar {
247        class_list = vec!["btn-toolbar".to_string()];
248    }
249
250    let class_list = class_list.join(" ");
251    let role = if props.toolbar { "toolbar" } else { "group" };
252    
253    rsx! {
254        div {
255            class: class_list,
256            role: role,
257            "aria-label": props.label,
258            {props.children}
259        }
260    }
261}
262
263#[derive(Clone, Props, PartialEq)]
264pub struct DropdownButtonProps {
265    #[props(optional)]
266    id: String,
267    #[props(optional, default = "".to_string())]
268    class: String,
269    #[props(optional, default = ButtonVariant::Primary)]
270    variant: ButtonVariant,
271    #[props(optional, default = Size::Normal)]
272    size: Size,
273    #[props(optional, default = false)]
274    disabled: bool,
275    #[props(optional, default = false)]
276    outline: bool,
277    #[props(optional, default = false)]
278    split: bool,
279    #[props(optional, default = "Dropdown".to_string())]
280    text: String,
281    children: Element,
282}
283
284#[component]
285pub fn DropdownButton(props: DropdownButtonProps) -> Element {
286    let mut class_list = vec!["btn".to_string(), "dropdown-toggle".to_string()];
287    
288    let variant: &str = props.variant.into();
289    if !variant.is_empty() {
290        if props.outline {
291            class_list.push(format!("btn-outline-{}", variant));
292        } else {
293            class_list.push(format!("btn-{}", variant));
294        }
295    }
296    
297    let size: &str = props.size.into();
298    if props.size != Size::Normal {
299        class_list.push(format!("btn-{}", size));
300    }
301    
302    // Add additional classes
303    if !props.class.is_empty() {
304        class_list.push(props.class.clone());
305    }
306    
307    let class_list = class_list.join(" ");
308    
309    if props.split {
310        rsx! {
311            div {
312                class: "btn-group",
313                button {
314                    r#type: "button",
315                    class: class_list.replace("dropdown-toggle", "").trim(),
316                    disabled: props.disabled,
317                    "{props.text}"
318                }
319                button {
320                    r#type: "button",
321                    class: format!("{} dropdown-toggle dropdown-toggle-split", class_list.replace("dropdown-toggle", "").trim()),
322                    "data-bs-toggle": "dropdown",
323                    "aria-expanded": "false",
324                    disabled: props.disabled,
325                    span {
326                        class: "visually-hidden",
327                        "Toggle Dropdown"
328                    }
329                }
330                ul {
331                    class: "dropdown-menu",
332                    {props.children}
333                }
334            }
335        }
336    } else {
337        rsx! {
338            div {
339                class: "dropdown",
340                button {
341                    id: props.id,
342                    r#type: "button",
343                    class: class_list,
344                    "data-bs-toggle": "dropdown",
345                    "aria-expanded": "false",
346                    disabled: props.disabled,
347                    "{props.text}"
348                }
349                ul {
350                    class: "dropdown-menu",
351                    {props.children}
352                }
353            }
354        }
355    }
356}