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}