adui_dioxus/components/
alert.rs

1use crate::components::icon::{Icon, IconKind};
2use dioxus::prelude::*;
3
4/// Semantic type of an Alert.
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum AlertType {
7    Success,
8    Info,
9    Warning,
10    Error,
11}
12
13impl AlertType {
14    fn as_class(&self) -> &'static str {
15        match self {
16            AlertType::Success => "adui-alert-success",
17            AlertType::Info => "adui-alert-info",
18            AlertType::Warning => "adui-alert-warning",
19            AlertType::Error => "adui-alert-error",
20        }
21    }
22
23    fn icon_kind(&self) -> IconKind {
24        match self {
25            AlertType::Success => IconKind::Check,
26            AlertType::Info => IconKind::Info,
27            AlertType::Warning => IconKind::Info,
28            AlertType::Error => IconKind::Close,
29        }
30    }
31}
32
33/// Props for the Alert component (MVP subset).
34#[derive(Props, Clone, PartialEq)]
35pub struct AlertProps {
36    /// Semantic type of the alert, controlling colors and default icon.
37    #[props(default = AlertType::Info)]
38    pub r#type: AlertType,
39    /// Main message content.
40    pub message: Element,
41    /// Optional detailed description.
42    #[props(optional)]
43    pub description: Option<Element>,
44    /// Whether to show the semantic icon.
45    #[props(default = true)]
46    pub show_icon: bool,
47    /// Whether the alert can be closed.
48    #[props(default)]
49    pub closable: bool,
50    /// Called when the close button is clicked.
51    #[props(optional)]
52    pub on_close: Option<EventHandler<()>>,
53    /// Optional custom icon element.
54    #[props(optional)]
55    pub icon: Option<Element>,
56    /// Whether the alert should be rendered as a banner (full width, compact).
57    #[props(default)]
58    pub banner: bool,
59    /// Extra class on the root element.
60    #[props(optional)]
61    pub class: Option<String>,
62    /// Inline style on the root element.
63    #[props(optional)]
64    pub style: Option<String>,
65}
66
67/// Ant Design flavored Alert (MVP: type + icon + closable).
68#[component]
69pub fn Alert(props: AlertProps) -> Element {
70    let AlertProps {
71        r#type,
72        message,
73        description,
74        show_icon,
75        closable,
76        on_close,
77        icon,
78        banner,
79        class,
80        style,
81    } = props;
82
83    let mut class_list = vec!["adui-alert".to_string(), r#type.as_class().to_string()];
84    if banner {
85        class_list.push("adui-alert-banner".into());
86    }
87    if let Some(extra) = class {
88        class_list.push(extra);
89    }
90    let class_attr = class_list.join(" ");
91    let style_attr = style.unwrap_or_default();
92
93    let on_close_cb = on_close;
94
95    // The visible flag allows the alert to hide itself after close when
96    // used in uncontrolled mode.
97    let visible = use_signal(|| true);
98
99    if !*visible.read() {
100        return VNode::empty();
101    }
102
103    rsx! {
104        div { class: "{class_attr}", style: "{style_attr}",
105            if show_icon {
106                div { class: "adui-alert-icon",
107                    if let Some(custom) = icon.clone() {
108                        {custom}
109                    } else {
110                        Icon { kind: r#type.icon_kind(), size: 16.0 }
111                    }
112                }
113            }
114            div { class: "adui-alert-content",
115                div { class: "adui-alert-message", {message} }
116                if let Some(desc) = description {
117                    div { class: "adui-alert-description", {desc} }
118                }
119            }
120            if closable {
121                button {
122                    r#type: "button",
123                    class: "adui-alert-close-icon",
124                    onclick: move |_| {
125                        if let Some(cb) = on_close_cb {
126                            cb.call(());
127                        }
128                        let mut v = visible;
129                        v.set(false);
130                    },
131                    Icon { kind: IconKind::Close, size: 12.0 }
132                }
133            }
134        }
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn alert_type_class_mapping_is_stable() {
144        assert_eq!(AlertType::Success.as_class(), "adui-alert-success");
145        assert_eq!(AlertType::Info.as_class(), "adui-alert-info");
146        assert_eq!(AlertType::Warning.as_class(), "adui-alert-warning");
147        assert_eq!(AlertType::Error.as_class(), "adui-alert-error");
148    }
149
150    #[test]
151    fn alert_type_icon_mapping() {
152        assert_eq!(AlertType::Success.icon_kind(), IconKind::Check);
153        assert_eq!(AlertType::Info.icon_kind(), IconKind::Info);
154        assert_eq!(AlertType::Warning.icon_kind(), IconKind::Info);
155        assert_eq!(AlertType::Error.icon_kind(), IconKind::Close);
156    }
157
158    #[test]
159    fn alert_type_all_variants() {
160        let variants = [
161            AlertType::Success,
162            AlertType::Info,
163            AlertType::Warning,
164            AlertType::Error,
165        ];
166        for variant in variants.iter() {
167            let class = variant.as_class();
168            assert!(!class.is_empty());
169            assert!(class.starts_with("adui-alert-"));
170            let icon = variant.icon_kind();
171            // Just verify icon_kind doesn't panic
172            let _ = format!("{:?}", icon);
173        }
174    }
175
176    #[test]
177    fn alert_type_equality() {
178        assert_eq!(AlertType::Success, AlertType::Success);
179        assert_eq!(AlertType::Info, AlertType::Info);
180        assert_ne!(AlertType::Success, AlertType::Error);
181    }
182
183    #[test]
184    fn alert_type_clone() {
185        let original = AlertType::Warning;
186        let cloned = original;
187        assert_eq!(original, cloned);
188        assert_eq!(original.as_class(), cloned.as_class());
189        assert_eq!(original.icon_kind(), cloned.icon_kind());
190    }
191
192    #[test]
193    fn alert_props_defaults() {
194        // AlertProps requires message, so we can't create a fully default instance
195        // But we can verify the default values:
196        // type defaults to AlertType::Info
197        // show_icon defaults to true
198        // closable defaults to false
199        // banner defaults to false
200    }
201
202    #[test]
203    fn alert_type_debug() {
204        let alert_type = AlertType::Error;
205        let debug_str = format!("{:?}", alert_type);
206        assert!(debug_str.contains("Error"));
207    }
208
209    #[test]
210    fn alert_type_all_icon_kinds() {
211        // Verify all icon kinds are valid
212        let success_icon = AlertType::Success.icon_kind();
213        let info_icon = AlertType::Info.icon_kind();
214        let warning_icon = AlertType::Warning.icon_kind();
215        let error_icon = AlertType::Error.icon_kind();
216
217        assert_eq!(success_icon, IconKind::Check);
218        assert_eq!(info_icon, IconKind::Info);
219        assert_eq!(warning_icon, IconKind::Info);
220        assert_eq!(error_icon, IconKind::Close);
221    }
222
223    #[test]
224    fn alert_type_class_prefix() {
225        // All alert type classes should start with "adui-alert-"
226        assert!(AlertType::Success.as_class().starts_with("adui-alert-"));
227        assert!(AlertType::Info.as_class().starts_with("adui-alert-"));
228        assert!(AlertType::Warning.as_class().starts_with("adui-alert-"));
229        assert!(AlertType::Error.as_class().starts_with("adui-alert-"));
230    }
231
232    #[test]
233    fn alert_type_unique_classes() {
234        // All alert type classes should be unique
235        let classes: Vec<&str> = vec![
236            AlertType::Success.as_class(),
237            AlertType::Info.as_class(),
238            AlertType::Warning.as_class(),
239            AlertType::Error.as_class(),
240        ];
241        for (i, class1) in classes.iter().enumerate() {
242            for (j, class2) in classes.iter().enumerate() {
243                if i != j {
244                    assert_ne!(class1, class2);
245                }
246            }
247        }
248    }
249
250    #[test]
251    fn alert_type_copy_semantics() {
252        // AlertType should be Copy, so we can use it multiple times
253        let alert_type = AlertType::Warning;
254        let class1 = alert_type.as_class();
255        let class2 = alert_type.as_class();
256        let icon1 = alert_type.icon_kind();
257        let icon2 = alert_type.icon_kind();
258        assert_eq!(class1, class2);
259        assert_eq!(icon1, icon2);
260    }
261}