adui_dioxus/components/
tag.rs

1use crate::components::icon::{Icon, IconKind};
2use dioxus::prelude::*;
3
4/// Preset tag colors aligned with Ant Design semantics (MVP subset).
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum TagColor {
7    Default,
8    Primary,
9    Success,
10    Warning,
11    Error,
12}
13
14impl TagColor {
15    fn as_class(&self) -> &'static str {
16        match self {
17            TagColor::Default => "adui-tag-default",
18            TagColor::Primary => "adui-tag-primary",
19            TagColor::Success => "adui-tag-success",
20            TagColor::Warning => "adui-tag-warning",
21            TagColor::Error => "adui-tag-error",
22        }
23    }
24}
25
26#[cfg(test)]
27mod tests {
28    use super::*;
29
30    #[test]
31    fn tag_color_class_mapping_is_stable() {
32        assert_eq!(TagColor::Default.as_class(), "adui-tag-default");
33        assert_eq!(TagColor::Primary.as_class(), "adui-tag-primary");
34        assert_eq!(TagColor::Success.as_class(), "adui-tag-success");
35        assert_eq!(TagColor::Warning.as_class(), "adui-tag-warning");
36        assert_eq!(TagColor::Error.as_class(), "adui-tag-error");
37    }
38}
39
40/// Props for the Tag component (MVP subset).
41#[derive(Props, Clone, PartialEq)]
42pub struct TagProps {
43    /// Preset color for the tag.
44    #[props(optional)]
45    pub color: Option<TagColor>,
46    /// Whether the tag can be closed.
47    #[props(default)]
48    pub closable: bool,
49    /// Called when the close icon is clicked.
50    #[props(optional)]
51    pub on_close: Option<EventHandler<()>>,
52    /// Whether the tag is checkable (togglable selection).
53    #[props(default)]
54    pub checkable: bool,
55    /// Controlled checked state for checkable tags.
56    #[props(optional)]
57    pub checked: Option<bool>,
58    /// Default checked state for uncontrolled checkable tags.
59    #[props(optional)]
60    pub default_checked: Option<bool>,
61    /// Called when the checked state changes.
62    #[props(optional)]
63    pub on_change: Option<EventHandler<bool>>,
64    /// Extra class for the tag.
65    #[props(optional)]
66    pub class: Option<String>,
67    /// Inline style for the tag.
68    #[props(optional)]
69    pub style: Option<String>,
70    /// Tag content.
71    pub children: Element,
72}
73
74/// Ant Design flavored Tag (MVP: preset colors, closable, simple checkable).
75#[component]
76pub fn Tag(props: TagProps) -> Element {
77    let TagProps {
78        color,
79        closable,
80        on_close,
81        checkable,
82        checked,
83        default_checked,
84        on_change,
85        class,
86        style,
87        children,
88    } = props;
89
90    let is_checked_controlled = checked.is_some();
91    let initial_checked = default_checked.unwrap_or(false);
92    let checked_signal: Signal<bool> = use_signal(|| initial_checked);
93    let current_checked = checked.unwrap_or_else(|| *checked_signal.read());
94
95    let mut class_list = vec!["adui-tag".to_string()];
96    if let Some(color_kind) = color {
97        class_list.push(color_kind.as_class().into());
98    }
99    if checkable {
100        class_list.push("adui-tag-checkable".into());
101        if current_checked {
102            class_list.push("adui-tag-checkable-checked".into());
103        }
104    }
105    if let Some(extra) = class {
106        class_list.push(extra);
107    }
108    let class_attr = class_list.join(" ");
109    let style_attr = style.unwrap_or_default();
110
111    let on_close_cb = on_close;
112    let on_change_cb = on_change;
113    let checked_signal_for_click = checked_signal;
114
115    rsx! {
116        span {
117            class: "{class_attr}",
118            style: "{style_attr}",
119            onclick: move |_| {
120                if checkable {
121                    let next = !current_checked;
122                    if !is_checked_controlled {
123                        let mut sig = checked_signal_for_click;
124                        sig.set(next);
125                    }
126                    if let Some(cb) = on_change_cb {
127                        cb.call(next);
128                    }
129                }
130            },
131            {children}
132            if closable {
133                button {
134                    r#type: "button",
135                    class: "adui-tag-close",
136                    onclick: move |evt| {
137                        evt.stop_propagation();
138                        if let Some(cb) = on_close_cb {
139                            cb.call(());
140                        }
141                    },
142                    Icon { kind: IconKind::Close, size: 12.0 }
143                }
144            }
145        }
146    }
147}