dioxus_bootstrap_css/button.rs
1use dioxus::prelude::*;
2
3use crate::types::{Color, Size};
4
5/// Bootstrap Button component.
6///
7/// Renders a `<button>` by default. When `href` is set, renders an `<a>` element
8/// instead (Bootstrap link-button pattern).
9///
10/// Accepts all standard HTML attributes via `extends = GlobalAttributes`.
11/// This means `title`, `data-bs-toggle`, `aria-label`, `id`, etc. all work.
12///
13/// # Bootstrap HTML → Dioxus
14///
15/// | HTML | Dioxus |
16/// |---|---|
17/// | `<button class="btn btn-primary">` | `Button { color: Color::Primary, "Text" }` |
18/// | `<button class="btn btn-outline-danger btn-sm">` | `Button { color: Color::Danger, outline: true, size: Size::Sm, "Text" }` |
19/// | `<button class="btn btn-success btn-lg" disabled>` | `Button { color: Color::Success, size: Size::Lg, disabled: true, "Text" }` |
20/// | `<a class="btn btn-primary" href="/page">` | `Button { color: Color::Primary, href: "/page", "Link" }` |
21/// | `<button class="btn btn-primary" title="Tip">` | `Button { color: Color::Primary, title: "Tip", "Text" }` |
22///
23/// ```rust,no_run
24/// rsx! {
25/// Button { color: Color::Primary, "Click me" }
26/// Button { color: Color::Danger, outline: true, size: Size::Sm, "Delete" }
27/// Button { color: Color::Success, disabled: true, "Saved" }
28/// Button { color: Color::Warning, onclick: move |_| { /* handler */ }, "Action" }
29/// // Link button — renders <a> instead of <button>:
30/// Button { color: Color::Primary, href: "/page", "Go to Page" }
31/// // HTML attributes work directly:
32/// Button { color: Color::Secondary, title: "Tooltip text", "Hover me" }
33/// Button { color: Color::Primary, "data-bs-toggle": "modal", "Open Modal" }
34/// }
35/// ```
36#[derive(Clone, PartialEq, Props)]
37pub struct ButtonProps {
38 /// Button color variant.
39 #[props(default)]
40 pub color: Color,
41 /// Use outline style instead of filled.
42 #[props(default)]
43 pub outline: bool,
44 /// Button size.
45 #[props(default)]
46 pub size: Size,
47 /// Whether the button is disabled.
48 #[props(default)]
49 pub disabled: bool,
50 /// When set, renders an `<a>` element instead of `<button>` (link-button pattern).
51 #[props(default)]
52 pub href: Option<String>,
53 /// HTML button type attribute (ignored when `href` is set).
54 #[props(default = "button".to_string())]
55 pub r#type: String,
56 /// Click event handler.
57 #[props(default)]
58 pub onclick: Option<EventHandler<MouseEvent>>,
59 /// Active (pressed) state.
60 #[props(default)]
61 pub active: bool,
62 /// Additional CSS classes.
63 #[props(default)]
64 pub class: String,
65 /// Any additional HTML attributes (title, data-bs-toggle, aria-*, id, etc.)
66 #[props(extends = GlobalAttributes)]
67 attributes: Vec<Attribute>,
68 /// Child elements.
69 pub children: Element,
70}
71
72#[component]
73pub fn Button(props: ButtonProps) -> Element {
74 let style = if props.outline { "btn-outline" } else { "btn" };
75 let color = props.color;
76 let color_class = format!("{style}-{color}");
77
78 let size_class = match props.size {
79 Size::Md => String::new(),
80 s => format!(" btn-{s}"),
81 };
82
83 let active_class = if props.active { " active" } else { "" };
84
85 let full_class = if props.class.is_empty() {
86 format!("btn {color_class}{size_class}{active_class}")
87 } else {
88 format!(
89 "btn {color_class}{size_class}{active_class} {}",
90 props.class
91 )
92 };
93
94 if let Some(href) = &props.href {
95 // Link-button: render <a> with role="button"
96 let disabled_class = if props.disabled { " disabled" } else { "" };
97 let link_class = format!("{full_class}{disabled_class}");
98 rsx! {
99 a {
100 class: "{link_class}",
101 href: "{href}",
102 role: "button",
103 onclick: move |evt| {
104 if let Some(handler) = &props.onclick {
105 handler.call(evt);
106 }
107 },
108 ..props.attributes,
109 {props.children}
110 }
111 }
112 } else {
113 rsx! {
114 button {
115 class: "{full_class}",
116 r#type: "{props.r#type}",
117 disabled: props.disabled,
118 onclick: move |evt| {
119 if let Some(handler) = &props.onclick {
120 handler.call(evt);
121 }
122 },
123 ..props.attributes,
124 {props.children}
125 }
126 }
127 }
128}
129
130/// Bootstrap ButtonGroup component.
131///
132/// ```rust,no_run
133/// rsx! {
134/// ButtonGroup {
135/// Button { color: Color::Primary, "Left" }
136/// Button { color: Color::Primary, "Middle" }
137/// Button { color: Color::Primary, "Right" }
138/// }
139/// }
140/// ```
141#[derive(Clone, PartialEq, Props)]
142pub struct ButtonGroupProps {
143 /// Button group size.
144 #[props(default)]
145 pub size: Size,
146 /// Additional CSS classes.
147 #[props(default)]
148 pub class: String,
149 /// Child elements (buttons).
150 pub children: Element,
151}
152
153#[component]
154pub fn ButtonGroup(props: ButtonGroupProps) -> Element {
155 let size_class = match props.size {
156 Size::Md => String::new(),
157 s => format!(" btn-group-{s}"),
158 };
159
160 let full_class = if props.class.is_empty() {
161 format!("btn-group{size_class}")
162 } else {
163 format!("btn-group{size_class} {}", props.class)
164 };
165
166 rsx! {
167 div {
168 class: "{full_class}",
169 role: "group",
170 {props.children}
171 }
172 }
173}
174
175/// Bootstrap ButtonToolbar — groups multiple ButtonGroups.
176///
177/// ```rust,no_run
178/// rsx! {
179/// ButtonToolbar {
180/// ButtonGroup {
181/// Button { color: Color::Primary, "1" }
182/// Button { color: Color::Primary, "2" }
183/// }
184/// ButtonGroup {
185/// Button { color: Color::Secondary, "A" }
186/// }
187/// }
188/// }
189/// ```
190#[derive(Clone, PartialEq, Props)]
191pub struct ButtonToolbarProps {
192 /// Additional CSS classes.
193 #[props(default)]
194 pub class: String,
195 /// Child elements (ButtonGroups).
196 pub children: Element,
197}
198
199#[component]
200pub fn ButtonToolbar(props: ButtonToolbarProps) -> Element {
201 let full_class = if props.class.is_empty() {
202 "btn-toolbar".to_string()
203 } else {
204 format!("btn-toolbar {}", props.class)
205 };
206
207 rsx! {
208 div {
209 class: "{full_class}",
210 role: "toolbar",
211 {props.children}
212 }
213 }
214}