radix_leptos_primitives/components/
alert.rs

1use leptos::callback::Callback;
2use leptos::children::Children;
3use leptos::prelude::*;
4use crate::utils::{merge_optional_classes, generate_id};
5
6/// Alert component with proper accessibility and styling variants
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub enum AlertVariant {
9    Default,
10    Destructive,
11    Warning,
12    Success,
13    Info,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub enum AlertSize {
18    Default,
19    Sm,
20    Lg,
21}
22
23impl AlertVariant {
24    pub fn as_str(&self) -> &'static str {
25        match self {
26            AlertVariant::Default => "default",
27            AlertVariant::Destructive => "destructive",
28            AlertVariant::Warning => "warning",
29            AlertVariant::Success => "success",
30            AlertVariant::Info => "info",
31        }
32    }
33}
34
35impl AlertSize {
36    pub fn as_str(&self) -> &'static str {
37        match self {
38            AlertSize::Default => "default",
39            AlertSize::Sm => "sm",
40            AlertSize::Lg => "lg",
41        }
42    }
43}
44
45/// Alert root component
46#[component]
47pub fn Alert(
48    /// Alert styling variant
49    #[prop(optional, default = AlertVariant::Default)]
50    variant: AlertVariant,
51    /// Alert size
52    #[prop(optional, default = AlertSize::Default)]
53    size: AlertSize,
54    /// Whether the alert is dismissible
55    #[prop(optional, default = false)]
56    _dismissible: bool,
57    /// Whether the alert is visible
58    #[prop(optional, default = true)]
59    visible: bool,
60    /// CSS classes
61    #[prop(optional)]
62    class: Option<String>,
63    /// CSS styles
64    #[prop(optional)]
65    style: Option<String>,
66    /// Dismiss event handler
67    #[prop(optional)]
68    on_dismiss: Option<Callback<()>>,
69    /// Child content
70    children: Children,
71) -> impl IntoView {
72    let ___alert_id = generate_id("alert");
73
74    // Build data attributes for styling
75    let data_variant = variant.as_str();
76    let data_size = size.as_str();
77
78    // Merge classes with data attributes for CSS targeting
79    let base_classes = "radix-alert";
80    let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
81        .unwrap_or_else(|| base_classes.to_string());
82
83    // Handle dismiss
84    let handle_dismiss = move |e: web_sys::MouseEvent| {
85        e.prevent_default();
86        if let Some(on_dismiss) = on_dismiss {
87            on_dismiss.run(());
88        }
89    };
90
91    // Handle keyboard events
92    let handle_keydown = move |e: web_sys::KeyboardEvent| {
93        if e.key().as_str() == "Escape" {
94            e.prevent_default();
95            if let Some(on_dismiss) = on_dismiss {
96                on_dismiss.run(());
97            }
98        }
99    };
100
101    if !visible {
102        return {
103            let _: () = view! { <></> };
104            ().into_any()
105        };
106    }
107
108    view! {
109        <div
110            class=combined_class
111            style=style
112            data-variant=data_variant
113            data-size=data_size
114            data-dismissible=_dismissible
115            role="alert"
116            aria-live="polite"
117            aria-atomic="true"
118            on:keydown=handle_keydown
119        >
120            {children()}
121            {if _dismissible {
122                view! {
123                    <button
124                        class="radix-alert-dismiss"
125                        aria-label="Dismiss alert"
126                        on:click=handle_dismiss
127                    >
128                        "×"
129                    </button>
130                }.into_any()
131            } else {
132                let _: () = view! {};
133                ().into_any()
134            }}
135        </div>
136    }
137    .into_any()
138}
139
140/// Alert Title component
141#[component]
142pub fn AlertTitle(
143    /// CSS classes
144    #[prop(optional)]
145    class: Option<String>,
146    /// CSS styles
147    #[prop(optional)]
148    style: Option<String>,
149    /// Child content
150    children: Children,
151) -> impl IntoView {
152    let base_classes = "radix-alert-title";
153    let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
154        .unwrap_or_else(|| base_classes.to_string());
155
156    view! {
157        <div
158            class=combined_class
159            style=style
160        >
161            {children()}
162        </div>
163    }
164}
165
166/// Alert Description component
167#[component]
168pub fn AlertDescription(
169    /// CSS classes
170    #[prop(optional)]
171    class: Option<String>,
172    /// CSS styles
173    #[prop(optional)]
174    style: Option<String>,
175    /// Child content
176    children: Children,
177) -> impl IntoView {
178    let base_classes = "radix-alert-description";
179    let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
180        .unwrap_or_else(|| base_classes.to_string());
181
182    view! {
183        <div
184            class=combined_class
185            style=style
186        >
187            {children()}
188        </div>
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use crate::{AlertSize, AlertVariant};
195    use proptest::prelude::*;
196use crate::utils::{merge_optional_classes, generate_id};
197
198    // 1. Basic Rendering Tests
199    #[test]
200    fn test_alert_variants() {
201        run_test(|| {
202            let variants = [
203                AlertVariant::Default,
204                AlertVariant::Destructive,
205                AlertVariant::Warning,
206                AlertVariant::Success,
207                AlertVariant::Info,
208            ];
209
210            for variant in variants {
211                assert!(!variant.as_str().is_empty());
212            }
213        });
214    }
215
216    #[test]
217    fn test_alert_sizes() {
218        run_test(|| {
219            let sizes = [AlertSize::Default, AlertSize::Sm, AlertSize::Lg];
220
221            for size in sizes {
222                assert!(!size.as_str().is_empty());
223            }
224        });
225    }
226
227    // 2. Props Validation Tests
228    #[test]
229    fn test_alert_default_values() {
230        run_test(|| {
231            let variant = AlertVariant::Default;
232            let size = AlertSize::Default;
233            let dismissible = false;
234            let visible = true;
235
236            assert_eq!(variant, AlertVariant::Default);
237            assert_eq!(size, AlertSize::Default);
238            assert!(!dismissible);
239            assert!(visible);
240        });
241    }
242
243    #[test]
244    fn test_alert_custom_values() {
245        run_test(|| {
246            let variant = AlertVariant::Success;
247            let size = AlertSize::Lg;
248            let dismissible = true;
249            let visible = true;
250
251            assert_eq!(variant, AlertVariant::Success);
252            assert_eq!(size, AlertSize::Lg);
253            assert!(dismissible);
254            assert!(visible);
255        });
256    }
257
258    #[test]
259    fn test_alert_destructive_variant() {
260        run_test(|| {
261            let variant = AlertVariant::Destructive;
262            let size = AlertSize::Sm;
263            let dismissible = true;
264            let visible = true;
265
266            assert_eq!(variant, AlertVariant::Destructive);
267            assert_eq!(size, AlertSize::Sm);
268            assert!(dismissible);
269            assert!(visible);
270        });
271    }
272
273    // 3. State Management Tests
274    #[test]
275    fn test_alert_visibility_state() {
276        run_test(|| {
277            let mut visible = true;
278            let dismissible = true;
279
280            // Initial state
281            assert!(visible);
282            assert!(dismissible);
283
284            // Dismiss alert
285            visible = false;
286
287            assert!(!visible);
288            assert!(dismissible);
289        });
290    }
291
292    #[test]
293    fn test_alertdismissible_state() {
294        run_test(|| {
295            let visible = true;
296            let mut dismissible = false;
297
298            // Initial state
299            assert!(visible);
300            assert!(!dismissible);
301
302            // Make dismissible
303            dismissible = true;
304
305            assert!(visible);
306            assert!(dismissible);
307        });
308    }
309
310    // 4. Event Handling Tests
311    #[test]
312    fn test_alert_dismiss_handling() {
313        run_test(|| {
314            let dismiss_clicked = true;
315            let dismissible = true;
316            let visible = true;
317
318            assert!(dismiss_clicked);
319            assert!(dismissible);
320            assert!(visible);
321
322            if dismiss_clicked && dismissible {}
323        });
324    }
325
326    #[test]
327    fn test_alert_keyboard_dismiss() {
328        run_test(|| {
329            let escape_pressed = true;
330            let dismissible = true;
331            let visible = true;
332
333            assert!(escape_pressed);
334            assert!(dismissible);
335            assert!(visible);
336
337            if escape_pressed && dismissible {}
338        });
339    }
340
341    // 5. Accessibility Tests
342    #[test]
343    fn test_alert_accessibility() {
344        run_test(|| {
345            let role = "alert";
346            let aria_live = "polite";
347            let aria_atomic = "true";
348            let aria_label = "Dismiss alert";
349
350            assert_eq!(role, "alert");
351            assert_eq!(aria_live, "polite");
352            assert_eq!(aria_atomic, "true");
353            assert_eq!(aria_label, "Dismiss alert");
354        });
355    }
356
357    // 6. Edge Case Tests
358    #[test]
359    fn test_alert_edge_cases() {
360        run_test(|| {
361            let variant = AlertVariant::Warning;
362            let size = AlertSize::Default;
363            let dismissible = false;
364            let visible = false;
365
366            assert_eq!(variant, AlertVariant::Warning);
367            assert_eq!(size, AlertSize::Default);
368            assert!(!dismissible);
369            assert!(!visible);
370        });
371    }
372
373    #[test]
374    fn test_alert_nondismissible() {
375        run_test(|| {
376            let variant = AlertVariant::Info;
377            let size = AlertSize::Lg;
378            let dismissible = false;
379            let visible = true;
380
381            assert_eq!(variant, AlertVariant::Info);
382            assert_eq!(size, AlertSize::Lg);
383            assert!(!dismissible);
384            assert!(visible);
385
386            // Non-dismissible alert should not respond to dismiss actions
387            let dismiss_clicked = true;
388            if dismiss_clicked && !dismissible {
389                // Alert should remain visible
390            }
391        });
392    }
393
394    #[test]
395    fn test_alert_invisible_state() {
396        run_test(|| {
397            let variant = AlertVariant::Success;
398            let size = AlertSize::Sm;
399            let dismissible = true;
400            let visible = false;
401
402            assert_eq!(variant, AlertVariant::Success);
403            assert_eq!(size, AlertSize::Sm);
404            assert!(dismissible);
405            assert!(!visible);
406
407            // Invisible alert should not be rendered
408            if !visible {}
409        });
410    }
411
412    // 7. Property-Based Tests
413    proptest! {
414        #[test]
415        fn test_alert_properties(
416            variant in prop::sample::select(&[
417                AlertVariant::Default,
418                AlertVariant::Destructive,
419                AlertVariant::Warning,
420                AlertVariant::Success,
421                AlertVariant::Info,
422            ]),
423            size in prop::sample::select(&[
424                AlertSize::Default,
425                AlertSize::Sm,
426                AlertSize::Lg,
427            ]),
428            dismissible in prop::bool::ANY,
429            visible in prop::bool::ANY
430        ) {
431            assert!(!variant.as_str().is_empty());
432            assert!(!size.as_str().is_empty());
433
434            // Test that boolean properties are properly typed
435            assert!(matches!(dismissible, true | false));
436            assert!(matches!(visible, true | false));
437
438            // Test dismiss behavior
439            if !visible {
440                // Invisible alert should not be interactive
441            }
442
443            if !dismissible {
444                // Non-dismissible alert should not respond to dismiss actions
445            }
446        }
447    }
448
449    // Helper function for running tests
450    fn run_test<F>(f: F)
451    where
452        F: FnOnce(),
453    {
454        f();
455    }
456}