radix_leptos_primitives/components/
toast.rs

1use crate::utils::merge_classes;
2use leptos::callback::Callback;
3use leptos::children::Children;
4use leptos::prelude::*;
5
6/// Toast component - Enhanced notification system with positioning
7#[component]
8pub fn Toast(
9    #[prop(optional)] class: Option<String>,
10    #[prop(optional)] style: Option<String>,
11    #[prop(optional)] children: Option<Children>,
12    #[prop(optional)] title: Option<String>,
13    #[prop(optional)] description: Option<String>,
14    #[prop(optional)] variant: Option<ToastVariant>,
15    #[prop(optional)] position: Option<ToastPosition>,
16    #[prop(optional)] duration: Option<u64>,
17    #[prop(optional)] dismissible: Option<bool>,
18    #[prop(optional)] on_dismiss: Option<Callback<()>>,
19    #[prop(optional)] on_action: Option<Callback<()>>,
20) -> impl IntoView {
21    let title = title.unwrap_or_default();
22    let description = description.unwrap_or_default();
23    let variant = variant.unwrap_or_default();
24    let position = position.unwrap_or_default();
25    let duration = duration.unwrap_or(5000);
26    let dismissible = dismissible.unwrap_or(true);
27
28    let class = merge_classes(
29        [
30            "toast",
31            variant.to_class(),
32            position.to_class(),
33            if dismissible {
34                "dismissible"
35            } else {
36                "non-dismissible"
37            },
38            class.as_deref().unwrap_or(""),
39        ]
40        .to_vec(),
41    );
42
43    view! {
44        <div
45            class=class
46            style=style
47            role="alert"
48            aria-live="polite"
49            aria-atomic="true"
50            data-duration=duration
51            data-position=position.to_string()
52            data-variant=variant.to_string()
53        >
54            {children.map(|c| c())}
55        </div>
56    }
57}
58
59/// Toast Provider component
60#[component]
61pub fn ToastProvider(
62    #[prop(optional)] class: Option<String>,
63    #[prop(optional)] style: Option<String>,
64    #[prop(optional)] children: Option<Children>,
65    #[prop(optional)] position: Option<ToastPosition>,
66    #[prop(optional)] max_toasts: Option<usize>,
67    #[prop(optional)] default_duration: Option<u64>,
68) -> impl IntoView {
69    let position = position.unwrap_or_default();
70    let max_toasts = max_toasts.unwrap_or(5);
71    let default_duration = default_duration.unwrap_or(5000);
72
73    let class = merge_classes(
74        [
75            "toast-provider",
76            position.to_class(),
77            class.as_deref().unwrap_or(""),
78        ]
79        .to_vec(),
80    );
81
82    view! {
83        <div
84            class=class
85            style=style
86            role="region"
87            aria-label="Toast notifications"
88            data-max-toasts=max_toasts
89            data-default-duration=default_duration
90            data-position=position.to_string()
91        >
92            {children.map(|c| c())}
93        </div>
94    }
95}
96
97/// Toast Title component
98#[component]
99pub fn ToastTitle(
100    #[prop(optional)] class: Option<String>,
101    #[prop(optional)] style: Option<String>,
102    #[prop(optional)] children: Option<Children>,
103    #[prop(optional)] title: Option<String>,
104) -> impl IntoView {
105    let title = title.unwrap_or_default();
106
107    let class = merge_classes(["toast-title", class.as_deref().unwrap_or("")].to_vec());
108
109    view! {
110        <div
111            class=class
112            style=style
113            role="heading"
114            data-level="3"
115        >
116            {children.map(|c| c())}
117        </div>
118    }
119}
120
121/// Toast Description component
122#[component]
123pub fn ToastDescription(
124    #[prop(optional)] class: Option<String>,
125    #[prop(optional)] style: Option<String>,
126    #[prop(optional)] children: Option<Children>,
127    #[prop(optional)] description: Option<String>,
128) -> impl IntoView {
129    let description = description.unwrap_or_default();
130
131    let class = merge_classes(["toast-description", class.as_deref().unwrap_or("")].to_vec());
132
133    view! {
134        <div
135            class=class
136            style=style
137            role="text"
138        >
139            {children.map(|c| c())}
140        </div>
141    }
142}
143
144/// Toast Action component
145#[component]
146pub fn ToastAction(
147    #[prop(optional)] class: Option<String>,
148    #[prop(optional)] style: Option<String>,
149    #[prop(optional)] children: Option<Children>,
150    #[prop(optional)] label: Option<String>,
151    #[prop(optional)] on_click: Option<Callback<()>>,
152) -> impl IntoView {
153    let label = label.unwrap_or_else(|| "Action".to_string());
154
155    let class = merge_classes(["toast-action", class.as_deref().unwrap_or("")].to_vec());
156
157    let handle_click = move |_| {
158        if let Some(callback) = on_click {
159            callback.run(());
160        }
161    };
162
163    view! {
164        <button
165            class=class
166            style=style
167            type="button"
168            aria-label=label
169            on:click=handle_click
170        >
171            {children.map(|c| c())}
172        </button>
173    }
174}
175
176/// Toast Close Button component
177#[component]
178pub fn ToastClose(
179    #[prop(optional)] class: Option<String>,
180    #[prop(optional)] style: Option<String>,
181    #[prop(optional)] children: Option<Children>,
182    #[prop(optional)] on_click: Option<Callback<()>>,
183) -> impl IntoView {
184    let class = merge_classes(["toast-close", class.as_deref().unwrap_or("")].to_vec());
185
186    let handle_click = move |_| {
187        if let Some(callback) = on_click {
188            callback.run(());
189        }
190    };
191
192    view! {
193        <button
194            class=class
195            style=style
196            type="button"
197            aria-label="Close toast"
198            on:click=handle_click
199        >
200            {children.map(|c| c())}
201        </button>
202    }
203}
204
205/// Toast Variant enum
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
207pub enum ToastVariant {
208    #[default]
209    Default,
210    Success,
211    Warning,
212    Error,
213    Info,
214}
215
216impl ToastVariant {
217    pub fn to_class(&self) -> &'static str {
218        match self {
219            ToastVariant::Default => "variant-default",
220            ToastVariant::Success => "variant-success",
221            ToastVariant::Warning => "variant-warning",
222            ToastVariant::Error => "variant-error",
223            ToastVariant::Info => "variant-info",
224        }
225    }
226
227    pub fn to_string(&self) -> &'static str {
228        match self {
229            ToastVariant::Default => "default",
230            ToastVariant::Success => "success",
231            ToastVariant::Warning => "warning",
232            ToastVariant::Error => "error",
233            ToastVariant::Info => "info",
234        }
235    }
236}
237
238/// Toast Position enum
239#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
240pub enum ToastPosition {
241    #[default]
242    TopRight,
243    TopLeft,
244    TopCenter,
245    BottomRight,
246    BottomLeft,
247    BottomCenter,
248}
249
250impl ToastPosition {
251    pub fn to_class(&self) -> &'static str {
252        match self {
253            ToastPosition::TopRight => "position-top-right",
254            ToastPosition::TopLeft => "position-top-left",
255            ToastPosition::TopCenter => "position-top-center",
256            ToastPosition::BottomRight => "position-bottom-right",
257            ToastPosition::BottomLeft => "position-bottom-left",
258            ToastPosition::BottomCenter => "position-bottom-center",
259        }
260    }
261
262    pub fn to_string(&self) -> &'static str {
263        match self {
264            ToastPosition::TopRight => "top-right",
265            ToastPosition::TopLeft => "top-left",
266            ToastPosition::TopCenter => "top-center",
267            ToastPosition::BottomRight => "bottom-right",
268            ToastPosition::BottomLeft => "bottom-left",
269            ToastPosition::BottomCenter => "bottom-center",
270        }
271    }
272}
273
274/// Toast Viewport component
275#[component]
276pub fn ToastViewport(
277    #[prop(optional)] class: Option<String>,
278    #[prop(optional)] style: Option<String>,
279    #[prop(optional)] children: Option<Children>,
280    #[prop(optional)] position: Option<ToastPosition>,
281) -> impl IntoView {
282    let position = position.unwrap_or_default();
283
284    let class = merge_classes(
285        [
286            "toast-viewport",
287            position.to_class(),
288            class.as_deref().unwrap_or(""),
289        ]
290        .to_vec(),
291    );
292
293    view! {
294        <div
295            class=class
296            style=style
297            role="region"
298            aria-label="Toast viewport"
299            data-position=position.to_string()
300        >
301            {children.map(|c| c())}
302        </div>
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use proptest::prelude::*;
309    use wasm_bindgen_test::*;
310
311    wasm_bindgen_test_configure!(run_in_browser);
312
313    // Unit Tests
314    #[test]
315    fn test_toast_creation() {}
316    #[test]
317    fn test_toast_with_class() {}
318    #[test]
319    fn test_toast_with_style() {}
320    #[test]
321    fn test_toast_title() {}
322    #[test]
323    fn test_toast_description() {}
324    #[test]
325    fn test_toast_variant() {}
326    #[test]
327    fn test_toast_position() {}
328    #[test]
329    fn test_toast_duration() {}
330    #[test]
331    fn test_toastdismissible() {}
332    #[test]
333    fn test_toast_on_dismiss() {}
334    #[test]
335    fn test_toast_on_action() {}
336
337    // Toast Provider tests
338    #[test]
339    fn test_toast_provider_creation() {}
340    #[test]
341    fn test_toast_provider_with_class() {}
342    #[test]
343    fn test_toast_provider_position() {}
344    #[test]
345    fn test_toast_provider_max_toasts() {}
346    #[test]
347    fn test_toast_provider_default_duration() {}
348
349    // Toast Title tests
350    #[test]
351    fn test_toast_title_creation() {}
352    #[test]
353    fn test_toast_title_with_class() {}
354    #[test]
355    fn test_toast_title_title() {}
356
357    // Toast Description tests
358    #[test]
359    fn test_toast_description_creation() {}
360    #[test]
361    fn test_toast_description_with_class() {}
362    #[test]
363    fn test_toast_description_description() {}
364
365    // Toast Action tests
366    #[test]
367    fn test_toast_action_creation() {}
368    #[test]
369    fn test_toast_action_with_class() {}
370    #[test]
371    fn test_toast_action_label() {}
372    #[test]
373    fn test_toast_action_on_click() {}
374
375    // Toast Close tests
376    #[test]
377    fn test_toast_close_creation() {}
378    #[test]
379    fn test_toast_close_with_class() {}
380    #[test]
381    fn test_toast_close_on_click() {}
382
383    // Toast Variant tests
384    #[test]
385    fn test_toast_variant_default() {}
386    #[test]
387    fn test_toast_variant_success() {}
388    #[test]
389    fn test_toast_variant_warning() {}
390    #[test]
391    fn test_toast_variant_error() {}
392    #[test]
393    fn test_toast_variant_info() {}
394
395    // Toast Position tests
396    #[test]
397    fn test_toast_position_default() {}
398    #[test]
399    fn test_toast_position_top_right() {}
400    #[test]
401    fn test_toast_position_top_left() {}
402    #[test]
403    fn test_toast_position_top_center() {}
404    #[test]
405    fn test_toast_position_bottom_right() {}
406    #[test]
407    fn test_toast_position_bottom_left() {}
408    #[test]
409    fn test_toast_position_bottom_center() {}
410
411    // Toast Viewport tests
412    #[test]
413    fn test_toast_viewport_creation() {}
414    #[test]
415    fn test_toast_viewport_with_class() {}
416    #[test]
417    fn test_toast_viewport_position() {}
418
419    // Helper function tests
420    #[test]
421    fn test_merge_classes_empty() {}
422    #[test]
423    fn test_merge_classes_single() {}
424    #[test]
425    fn test_merge_classes_multiple() {}
426    #[test]
427    fn test_merge_classes_with_empty() {}
428
429    // Property-based Tests
430    #[test]
431    fn test_toast_property_based() {
432        proptest!(|(____class in ".*", __style in ".*")| {
433
434        });
435    }
436
437    #[test]
438    fn test_toast_duration_validation() {
439        proptest!(|(____duration in 1000..30000u64)| {
440
441        });
442    }
443
444    #[test]
445    fn test_toast_position_validation() {
446        proptest!(|(____position in ".*")| {
447
448        });
449    }
450
451    // Integration Tests
452    #[test]
453    fn test_toast_notification_workflow() {}
454    #[test]
455    fn test_toast_accessibility() {}
456    #[test]
457    fn test_toast_positioning_system() {}
458    #[test]
459    fn test_toast_dismissal_workflow() {}
460    #[test]
461    fn test_toast_action_workflow() {}
462
463    // Performance Tests
464    #[test]
465    fn test_toast_multiple_notifications() {}
466    #[test]
467    fn test_toast_render_performance() {}
468    #[test]
469    fn test_toast_memory_usage() {}
470    #[test]
471    fn test_toast_animation_performance() {}
472}