Skip to main content

fc_dioxus/components/
badge.rs

1//! Status labels, interactive tags, and keyboard keys from badge.css.
2use crate::{
3    core::{
4        assets,
5        attrs::{availability_attributes, component_attributes},
6    },
7    prelude::*,
8    theme::use_stylesheets,
9};
10const BEM: Bem = Bem::new("fc-badge");
11const TAG: Bem = Bem::new("fc-tag");
12const KBD: Bem = Bem::new("fc-kbd");
13fc_modifier! {
14    /// Badge tones, including upstream's checked solid accent pair.
15    pub enum BadgeTone for BEM {
16        /// Neutral label.
17        Neutral => None,
18        /// Accent tint.
19        Accent => "accent",
20        /// Information tint.
21        Info => "info",
22        /// Success tint.
23        Success => "success",
24        /// Warning tint.
25        Warning => "warning",
26        /// Error tint.
27        Danger => "danger",
28        /// Solid accent; does not imply a solid version of every tone.
29        Solid => "solid",
30    }
31}
32fc_modifier! { enum BadgeFill for BEM { Filled => None, Outline => "outline" } }
33fc_modifier! { enum BadgeShape for BEM { Pill => None, Square => "square" } }
34fc_modifier! { enum BadgeNumber for BEM { Text => None, Count => "count" } }
35/// A static status label. The visible text supplies meaning independently of color.
36#[component]
37pub fn Badge(
38    #[props(default)] tone: BadgeTone,
39    #[props(default)] solid: bool,
40    #[props(default)] outline: bool,
41    #[props(default)] square: bool,
42    #[props(default)] dot: bool,
43    #[props(default)] count: bool,
44    #[props(default)] clipped: bool,
45    #[props(default)] class: Option<String>,
46    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
47    children: Element,
48) -> Element {
49    let classes = ClassList::new(BEM.block())
50        .with_modifier(BEM, tone)
51        .with_modifier(
52            BEM,
53            if solid {
54                BadgeTone::Solid
55            } else {
56                BadgeTone::Neutral
57            },
58        )
59        .with_modifier(
60            BEM,
61            if outline {
62                BadgeFill::Outline
63            } else {
64                BadgeFill::Filled
65            },
66        )
67        .with_modifier(
68            BEM,
69            if square {
70                BadgeShape::Square
71            } else {
72                BadgeShape::Pill
73            },
74        )
75        .with_modifier(
76            BEM,
77            if count {
78                BadgeNumber::Count
79            } else {
80                BadgeNumber::Text
81            },
82        )
83        .extend(class.as_deref());
84    let attributes = component_attributes(attributes, classes, &["tabindex"]);
85    rsx! {
86        if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_BADGE_CSS } }
87        span { ..attributes,
88            if dot { span { class: BEM.element("dot"), aria_hidden: "true" } }
89            if clipped { span { class: BEM.element("label"), {children} } } else { {children} }
90        }
91    }
92}
93/// The named removal action of a token.
94#[derive(Clone, PartialEq)]
95pub struct TagRemove {
96    /// Name describing which token is removed.
97    pub label: AccessibleName,
98    /// Request removal; the owner retains state and restores focus.
99    pub onremove: EventHandler<()>,
100}
101/// A controlled native toggle, or a token with its own named remove button.
102#[component]
103pub fn Tag(
104    #[props(default)] pressed: bool,
105    #[props(default)] onchange: Option<EventHandler<bool>>,
106    #[props(default)] remove: Option<TagRemove>,
107    #[props(default)] availability: Availability,
108    #[props(default)] class: Option<String>,
109    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
110    children: Element,
111) -> Element {
112    let mut attributes = component_attributes(
113        attributes,
114        ClassList::new(TAG.block()).extend(class.as_deref()),
115        &[
116            "type",
117            "role",
118            "aria-pressed",
119            "aria-disabled",
120            "disabled",
121            "onclick",
122        ],
123    );
124    let state = availability_attributes(&availability);
125    let blocked = availability != Availability::Enabled;
126    if remove.is_none() {
127        attributes.extend(state.clone());
128        attributes.push(Attribute::new(
129            "aria-pressed",
130            pressed.to_string(),
131            None,
132            false,
133        ));
134    }
135    rsx! {
136        if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_BADGE_CSS } }
137        if let Some(remove) = remove {
138            span { ..attributes,
139                span { class: TAG.element("label"), {children} }
140                button { r#type: "button", class: TAG.element("remove"), aria_label: remove.label.to_string(),
141                    onclick: move |event| { if blocked { event.prevent_default(); } else { remove.onremove.call(()); } },
142                    ..state, "×"
143                }
144            }
145        } else {
146            button { r#type: "button",
147                onclick: move |event| { if blocked { event.prevent_default(); event.stop_propagation(); } else if let Some(onchange) = onchange { onchange.call(!pressed); } },
148                ..attributes, {children}
149            }
150        }
151    }
152}
153/// A visible keyboard key, using native keyboard-text semantics.
154#[component]
155pub fn Kbd(
156    #[props(default)] class: Option<String>,
157    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
158    children: Element,
159) -> Element {
160    let attributes = component_attributes(
161        attributes,
162        ClassList::new(KBD.block()).extend(class.as_deref()),
163        &[],
164    );
165    rsx! {
166        if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_BADGE_CSS } }
167        kbd { ..attributes, {children} }
168    }
169}