radix_leptos_primitives/components/
toast.rs1use crate::utils::merge_classes;
2use leptos::callback::Callback;
3use leptos::children::Children;
4use leptos::prelude::*;
5
6#[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#[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#[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#[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#[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#[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#[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#[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#[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}