use crate::{
core::{
assets,
attrs::{availability_attributes, component_attributes},
},
prelude::*,
theme::use_stylesheets,
};
const BEM: Bem = Bem::new("fc-btn");
fc_modifier! {
pub enum ButtonVariant for BEM {
Neutral => None,
Primary => "primary",
Secondary => "secondary",
Ghost => "ghost",
Tertiary => "tertiary",
Ok => "ok",
Cancel => "cancel",
Subtle => "subtle",
Danger => "danger",
}
}
fc_modifier! {
pub enum ButtonSize for BEM {
Md => None,
Sm => "sm",
Lg => "lg",
}
}
fc_modifier! { enum ButtonWidth for BEM { Auto => None, Block => "block" } }
fc_modifier! { enum ButtonShape for BEM { Text => None, Icon => "icon" } }
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ButtonType {
#[default]
Button,
Submit,
Reset,
}
impl ButtonType {
fn as_str(self) -> &'static str {
match self {
Self::Button => "button",
Self::Submit => "submit",
Self::Reset => "reset",
}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct ButtonProps {
#[props(default)]
pub variant: ButtonVariant,
#[props(default)]
pub size: ButtonSize,
#[props(default)]
pub block: bool,
#[props(default)]
pub availability: Availability,
#[props(default)]
pub busy: bool,
#[props(default)]
pub pressed: Option<bool>,
#[props(default)]
pub r#type: ButtonType,
#[props(default)]
pub icon_label: Option<AccessibleName>,
#[props(default)]
pub onclick: Option<EventHandler<MouseEvent>>,
#[props(default)]
pub class: Option<String>,
#[props(extends = GlobalAttributes, extends = button)]
pub attributes: Vec<Attribute>,
pub children: Element,
}
#[component]
pub fn Button(props: ButtonProps) -> Element {
let blocked = props.busy || props.availability != Availability::Enabled;
let classes = ClassList::new(BEM.block())
.with_modifier(BEM, props.variant)
.with_modifier(BEM, props.size)
.with_modifier(
BEM,
if props.block {
ButtonWidth::Block
} else {
ButtonWidth::Auto
},
)
.with_modifier(
BEM,
if props.icon_label.is_some() {
ButtonShape::Icon
} else {
ButtonShape::Text
},
)
.extend(super::group_item_class().as_deref())
.extend(props.class.as_deref());
let mut attributes = component_attributes(
props.attributes,
classes,
&[
"type",
"role",
"disabled",
"aria-disabled",
"aria-busy",
"aria-pressed",
"onclick",
],
);
attributes.extend(availability_attributes(&props.availability));
if let Some(super::popover::DescribedBy(tip)) = try_consume_context() {
match attributes
.iter_mut()
.find(|attr| attr.name == "aria-describedby")
{
Some(attr) => {
if let dioxus::core::AttributeValue::Text(ids) = &attr.value {
attr.value = dioxus::core::AttributeValue::Text(format!("{ids} {tip}"));
}
}
None => attributes.push(Attribute::new(
"aria-describedby",
tip.to_string(),
None,
false,
)),
}
}
if let Some(label) = &props.icon_label {
attributes.retain(|attr| !matches!(attr.name, "aria-label" | "aria-labelledby"));
attributes.push(Attribute::new("aria-label", label.to_string(), None, false));
}
attributes.push(Attribute::new("type", props.r#type.as_str(), None, false));
if props.busy {
attributes.push(Attribute::new("aria-busy", "true", None, false));
}
if let Some(pressed) = props.pressed {
attributes.push(Attribute::new(
"aria-pressed",
pressed.to_string(),
None,
false,
));
}
rsx! {
if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_BUTTON_CSS } }
button {
onclick: move |event: MouseEvent| {
if blocked { event.prevent_default(); event.stop_propagation(); }
else if let Some(handler) = props.onclick { handler.call(event); }
},
..attributes,
if props.busy { span { class: BEM.element("spinner"), aria_hidden: "true" } }
if props.busy { span { {props.children} } }
else { {props.children} }
}
}
}
#[component]
pub fn LinkButton(
href: String,
#[props(default)] variant: ButtonVariant,
#[props(default)] size: ButtonSize,
#[props(default)] block: bool,
#[props(default)]
icon_label: Option<AccessibleName>,
#[props(default)] availability: Availability,
#[props(default)]
onclick: Option<EventHandler<MouseEvent>>,
#[props(default)] class: Option<String>,
#[props(extends = GlobalAttributes, extends = a)] attributes: Vec<Attribute>,
children: Element,
) -> Element {
let blocked = availability != Availability::Enabled;
let link = crate::core::link::use_link((!blocked).then_some(href));
let classes = ClassList::new(BEM.block())
.with_modifier(BEM, variant)
.with_modifier(BEM, size)
.with_modifier(
BEM,
if block {
ButtonWidth::Block
} else {
ButtonWidth::Auto
},
)
.with_modifier(
BEM,
if icon_label.is_some() {
ButtonShape::Icon
} else {
ButtonShape::Text
},
)
.extend(super::group_item_class().as_deref())
.extend(class.as_deref());
let mut attributes = component_attributes(
attributes,
classes,
&["href", "aria-disabled", "aria-current", "onclick"],
);
match &availability {
Availability::Enabled => {}
Availability::Disabled => {
attributes.push(Attribute::new("aria-disabled", "true", None, false));
}
other => attributes.extend(availability_attributes(other)),
}
if let Some(label) = &icon_label {
attributes.retain(|attr| !matches!(attr.name, "aria-label" | "aria-labelledby"));
attributes.push(Attribute::new("aria-label", label.to_string(), None, false));
}
let href = link.href.clone();
let current = link.current;
rsx! {
if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_BUTTON_CSS } }
a { href, aria_current: current.then_some("page"),
onclick: move |event: MouseEvent| {
if blocked {
event.prevent_default();
return;
}
link.follow(&event);
if let Some(onclick) = onclick { onclick.call(event); }
},
..attributes, {children}
}
}
}