use dioxus::prelude::*;
use crate::{
cn,
uikit::{Accent, BUTTON_BASE, ButtonVariant, Size, button_accent_class, button_size_class},
};
pub fn button_classes(variant: &ButtonVariant, size: Size, icon: bool, accent: Option<Accent>, class: &str) -> String {
cn!(
BUTTON_BASE,
variant.as_class(),
button_size_class(size, icon),
accent.map_or("", |a| button_accent_class(a, variant)),
class
)
}
#[component]
pub fn Button(
#[props(default)] variant: ButtonVariant,
#[props(default)] size: Size,
#[props(default)] icon: bool,
accent: Option<Accent>,
#[props(default)] class: String,
#[props(default)] disabled: bool,
href: Option<String>,
r#type: Option<String>,
onclick: Option<EventHandler<MouseEvent>>,
children: Element,
) -> Element {
let cls = button_classes(&variant, size, icon, accent, &class);
if let Some(href) = href {
return rsx! {
a {
class: cls,
"data-slot": "button",
href,
onclick: move |e| { if let Some(h) = onclick { h.call(e); } },
{children}
}
};
}
rsx! {
button {
class: cls,
"data-slot": "button",
r#type,
disabled,
onclick: move |e| { if let Some(h) = onclick { h.call(e); } },
{children}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::uikit::test_util::render;
#[test]
fn default_variant_and_size_render() {
fn app() -> Element {
rsx! { Button { "go" } }
}
let html = render(app);
assert!(html.contains("bg-primary"), "{html}");
assert!(html.contains("h-9"), "{html}");
assert!(html.contains("go"));
assert!(html.contains("data-slot=\"button\""));
}
#[test]
fn icon_sm_size_is_canon_only_here() {
fn app() -> Element {
rsx! {
Button { size: Size::Sm, icon: true, "x" }
}
}
let html = render(app);
assert!(html.contains("h-8 aspect-square"), "{html}");
}
#[test]
fn icon_lg_drops_text_padding() {
fn app() -> Element {
rsx! {
Button { size: Size::Lg, icon: true, "x" }
}
}
let html = render(app);
assert!(html.contains("h-10 aspect-square"), "{html}");
assert!(!html.contains("px-6"), "icon button must not carry text padding: {html}");
}
#[test]
fn type_is_absent_unless_asked_for() {
fn app() -> Element {
rsx! { Button { "go" } }
}
let html = render(app);
assert!(!html.contains("type="), "bare Button mirrors TS and emits no type: {html}");
}
#[test]
fn type_prop_reaches_the_element() {
fn app() -> Element {
rsx! {
Button { r#type: "button", "go" }
}
}
let html = render(app);
assert!(html.contains("type=\"button\""), "{html}");
}
#[test]
fn accent_recolours_per_variant() {
fn filled() -> Element {
rsx! {
Button { accent: Accent::Warn, "x" }
}
}
let html = render(filled);
assert!(html.contains("bg-accent-warn"), "{html}");
assert!(!html.contains("bg-primary"), "the accent replaces the variant's fill: {html}");
fn outlined() -> Element {
rsx! {
Button { variant: ButtonVariant::Outline, accent: Accent::Warn, "x" }
}
}
let html = render(outlined);
assert!(html.contains("border-accent-warn/40"), "{html}");
assert!(!html.contains("bg-accent-warn "), "an outlined button stays outlined: {html}");
}
#[test]
fn class_override_wins() {
fn app() -> Element {
rsx! {
Button { class: "px-6", "x" }
}
}
let html = render(app);
assert!(html.contains("px-6"));
assert!(!html.contains("px-4"), "override should drop base px-4: {html}");
}
}