Skip to main content

dioxus_bootstrap_css/
badge.rs

1use dioxus::prelude::*;
2
3use crate::types::{BadgeFill, Color};
4
5/// Bootstrap Badge component.
6///
7/// # Bootstrap HTML → Dioxus
8///
9/// | HTML | Dioxus |
10/// |---|---|
11/// | `<span class="badge text-bg-primary">New</span>` | `Badge { color: Color::Primary, "New" }` |
12/// | `<span class="badge rounded-pill text-bg-danger">99+</span>` | `Badge { color: Color::Danger, pill: true, "99+" }` |
13/// | `<span class="badge bg-secondary">Alias</span>` | `Badge { color: Color::Secondary, fill: BadgeFill::Bg, "Alias" }` |
14/// | `<span class="badge bg-info-subtle text-info-emphasis">2 fields</span>` | `Badge { color: Color::Info, fill: BadgeFill::Subtle, "2 fields" }` |
15/// | `<span class="badge text-bg-secondary" role="button">Open</span>` | `Badge { color: Color::Secondary, onclick: move |_| {}, "Open" }` |
16///
17/// ```rust,no_run
18/// # use dioxus::prelude::*;
19/// # use dioxus_bootstrap_css::prelude::*;
20/// # fn _doctest() -> Element {
21/// rsx! {
22///     Badge { color: Color::Primary, "New" }
23///     Badge { color: Color::Danger, pill: true, "99+" }
24///     Badge { color: Color::Info, fill: BadgeFill::Subtle, "2 fields" }
25///     // Inside a heading
26///     h1 { "Messages " Badge { color: Color::Info, "4" } }
27/// }
28/// # }
29/// ```
30#[derive(Clone, PartialEq, Props)]
31pub struct BadgeProps {
32    /// Badge color variant.
33    #[props(default = Color::Primary)]
34    pub color: Color,
35    /// Which Bootstrap colour idiom to paint the badge with. Defaults to
36    /// `text-bg-<color>`, so existing callers are unchanged.
37    #[props(default)]
38    pub fill: BadgeFill,
39    /// Use pill (rounded) style.
40    #[props(default)]
41    pub pill: bool,
42    /// Additional CSS classes.
43    #[props(default)]
44    pub class: String,
45    /// Click event handler.
46    #[props(default)]
47    pub onclick: Option<EventHandler<MouseEvent>>,
48    /// Any additional HTML attributes.
49    #[props(extends = GlobalAttributes)]
50    attributes: Vec<Attribute>,
51    /// Child elements.
52    pub children: Element,
53}
54
55/// The badge's class string. Extracted so the colour idioms are assertable
56/// without rendering — the whole point of the `fill` prop is which classes come
57/// out, so that is what the tests pin.
58fn badge_class(color: Color, fill: BadgeFill, pill: bool, class: &str) -> String {
59    let color_classes = match fill {
60        BadgeFill::TextBg => format!(" text-bg-{color}"),
61        BadgeFill::Bg => format!(" bg-{color}"),
62        BadgeFill::Subtle => format!(" bg-{color}-subtle text-{color}-emphasis"),
63        BadgeFill::None => String::new(),
64    };
65    let pill = if pill { " rounded-pill" } else { "" };
66    if class.is_empty() {
67        format!("badge{color_classes}{pill}")
68    } else {
69        format!("badge{color_classes}{pill} {class}")
70    }
71}
72
73#[component]
74pub fn Badge(props: BadgeProps) -> Element {
75    let full_class = badge_class(props.color, props.fill, props.pill, &props.class);
76
77    rsx! {
78        span {
79            class: "{full_class}",
80            onclick: move |evt| {
81                if let Some(handler) = &props.onclick {
82                    handler.call(evt);
83                }
84            },
85            ..props.attributes,
86            {props.children}
87        }
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn badge_default_fill_is_text_bg() {
97        // The default must stay `text-bg-*`: it is what every existing caller
98        // renders today, so the new prop has to be invisible when unset.
99        assert_eq!(
100            badge_class(Color::Primary, BadgeFill::default(), false, ""),
101            "badge text-bg-primary"
102        );
103    }
104
105    #[test]
106    fn badge_bg_fill_omits_the_foreground() {
107        assert_eq!(
108            badge_class(Color::Secondary, BadgeFill::Bg, false, ""),
109            "badge bg-secondary"
110        );
111    }
112
113    #[test]
114    fn badge_subtle_fill_emits_both_halves_of_the_pair() {
115        // Subtle is one idiom, not two utilities: the background is unreadable
116        // without the emphasis foreground, so both must always appear together.
117        assert_eq!(
118            badge_class(Color::Info, BadgeFill::Subtle, false, ""),
119            "badge bg-info-subtle text-info-emphasis"
120        );
121    }
122
123    #[test]
124    fn badge_none_fill_emits_geometry_only() {
125        assert_eq!(
126            badge_class(Color::Primary, BadgeFill::None, false, ""),
127            "badge"
128        );
129    }
130
131    #[test]
132    fn badge_pill_and_extra_classes_survive_every_fill() {
133        assert_eq!(
134            badge_class(Color::Danger, BadgeFill::TextBg, true, "ms-2"),
135            "badge text-bg-danger rounded-pill ms-2"
136        );
137        assert_eq!(
138            badge_class(Color::Danger, BadgeFill::None, true, "ms-2"),
139            "badge rounded-pill ms-2"
140        );
141    }
142}