1use crate::{
3 core::{
4 assets,
5 attrs::{availability_attributes, component_attributes},
6 },
7 prelude::*,
8 theme::use_stylesheets,
9};
10
11const BEM: Bem = Bem::new("fc-btn");
12fc_modifier! {
13 pub enum ButtonVariant for BEM {
15 Neutral => None,
17 Primary => "primary",
19 Secondary => "secondary",
21 Ghost => "ghost",
23 Tertiary => "tertiary",
25 Ok => "ok",
27 Cancel => "cancel",
29 Subtle => "subtle",
31 Danger => "danger",
33 }
34}
35fc_modifier! {
36 pub enum ButtonSize for BEM {
38 Md => None,
40 Sm => "sm",
42 Lg => "lg",
44 }
45}
46fc_modifier! { enum ButtonWidth for BEM { Auto => None, Block => "block" } }
47fc_modifier! { enum ButtonShape for BEM { Text => None, Icon => "icon" } }
48#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
50#[non_exhaustive]
51pub enum ButtonType {
52 #[default]
54 Button,
55 Submit,
57 Reset,
59}
60impl ButtonType {
61 fn as_str(self) -> &'static str {
62 match self {
63 Self::Button => "button",
64 Self::Submit => "submit",
65 Self::Reset => "reset",
66 }
67 }
68}
69#[derive(Props, Clone, PartialEq)]
71pub struct ButtonProps {
72 #[props(default)]
74 pub variant: ButtonVariant,
75 #[props(default)]
77 pub size: ButtonSize,
78 #[props(default)]
80 pub block: bool,
81 #[props(default)]
83 pub availability: Availability,
84 #[props(default)]
86 pub busy: bool,
87 #[props(default)]
89 pub pressed: Option<bool>,
90 #[props(default)]
92 pub r#type: ButtonType,
93 #[props(default)]
95 pub icon_label: Option<AccessibleName>,
96 #[props(default)]
98 pub onclick: Option<EventHandler<MouseEvent>>,
99 #[props(default)]
101 pub class: Option<String>,
102 #[props(extends = GlobalAttributes, extends = button)]
104 pub attributes: Vec<Attribute>,
105 pub children: Element,
107}
108#[component]
110pub fn Button(props: ButtonProps) -> Element {
111 let blocked = props.busy || props.availability != Availability::Enabled;
112 let classes = ClassList::new(BEM.block())
113 .with_modifier(BEM, props.variant)
114 .with_modifier(BEM, props.size)
115 .with_modifier(
116 BEM,
117 if props.block {
118 ButtonWidth::Block
119 } else {
120 ButtonWidth::Auto
121 },
122 )
123 .with_modifier(
124 BEM,
125 if props.icon_label.is_some() {
126 ButtonShape::Icon
127 } else {
128 ButtonShape::Text
129 },
130 )
131 .extend(super::group_item_class().as_deref())
132 .extend(props.class.as_deref());
133 let mut attributes = component_attributes(
134 props.attributes,
135 classes,
136 &[
137 "type",
138 "role",
139 "disabled",
140 "aria-disabled",
141 "aria-busy",
142 "aria-pressed",
143 "onclick",
144 ],
145 );
146 attributes.extend(availability_attributes(&props.availability));
147 if let Some(super::popover::DescribedBy(tip)) = try_consume_context() {
149 match attributes
150 .iter_mut()
151 .find(|attr| attr.name == "aria-describedby")
152 {
153 Some(attr) => {
154 if let dioxus::core::AttributeValue::Text(ids) = &attr.value {
155 attr.value = dioxus::core::AttributeValue::Text(format!("{ids} {tip}"));
156 }
157 }
158 None => attributes.push(Attribute::new(
159 "aria-describedby",
160 tip.to_string(),
161 None,
162 false,
163 )),
164 }
165 }
166 if let Some(label) = &props.icon_label {
167 attributes.retain(|attr| !matches!(attr.name, "aria-label" | "aria-labelledby"));
168 attributes.push(Attribute::new("aria-label", label.to_string(), None, false));
169 }
170 attributes.push(Attribute::new("type", props.r#type.as_str(), None, false));
171 if props.busy {
172 attributes.push(Attribute::new("aria-busy", "true", None, false));
173 }
174 if let Some(pressed) = props.pressed {
175 attributes.push(Attribute::new(
176 "aria-pressed",
177 pressed.to_string(),
178 None,
179 false,
180 ));
181 }
182 rsx! {
183 if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_BUTTON_CSS } }
184 button {
185 onclick: move |event: MouseEvent| {
186 if blocked { event.prevent_default(); event.stop_propagation(); }
187 else if let Some(handler) = props.onclick { handler.call(event); }
188 },
189 ..attributes,
190 if props.busy { span { class: BEM.element("spinner"), aria_hidden: "true" } }
191 if props.busy { span { {props.children} } }
192 else { {props.children} }
193 }
194 }
195}
196
197#[component]
205pub fn LinkButton(
206 href: String,
207 #[props(default)] variant: ButtonVariant,
208 #[props(default)] size: ButtonSize,
209 #[props(default)] block: bool,
210 #[props(default)]
212 icon_label: Option<AccessibleName>,
213 #[props(default)] availability: Availability,
214 #[props(default)]
216 onclick: Option<EventHandler<MouseEvent>>,
217 #[props(default)] class: Option<String>,
218 #[props(extends = GlobalAttributes, extends = a)] attributes: Vec<Attribute>,
219 children: Element,
220) -> Element {
221 let blocked = availability != Availability::Enabled;
222 let link = crate::core::link::use_link((!blocked).then_some(href));
223 let classes = ClassList::new(BEM.block())
224 .with_modifier(BEM, variant)
225 .with_modifier(BEM, size)
226 .with_modifier(
227 BEM,
228 if block {
229 ButtonWidth::Block
230 } else {
231 ButtonWidth::Auto
232 },
233 )
234 .with_modifier(
235 BEM,
236 if icon_label.is_some() {
237 ButtonShape::Icon
238 } else {
239 ButtonShape::Text
240 },
241 )
242 .extend(super::group_item_class().as_deref())
243 .extend(class.as_deref());
244 let mut attributes = component_attributes(
245 attributes,
246 classes,
247 &["href", "aria-disabled", "aria-current", "onclick"],
248 );
249 match &availability {
250 Availability::Enabled => {}
251 Availability::Disabled => {
252 attributes.push(Attribute::new("aria-disabled", "true", None, false));
253 }
254 other => attributes.extend(availability_attributes(other)),
255 }
256 if let Some(label) = &icon_label {
257 attributes.retain(|attr| !matches!(attr.name, "aria-label" | "aria-labelledby"));
258 attributes.push(Attribute::new("aria-label", label.to_string(), None, false));
259 }
260 let href = link.href.clone();
261 let current = link.current;
262 rsx! {
263 if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_BUTTON_CSS } }
264 a { href, aria_current: current.then_some("page"),
265 onclick: move |event: MouseEvent| {
266 if blocked {
267 event.prevent_default();
268 return;
269 }
270 link.follow(&event);
271 if let Some(onclick) = onclick { onclick.call(event); }
272 },
273 ..attributes, {children}
274 }
275 }
276}