radix_leptos_primitives/components/
alert.rs1use leptos::callback::Callback;
2use leptos::children::Children;
3use leptos::prelude::*;
4use crate::utils::{merge_optional_classes, generate_id};
5
6#[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#[component]
47pub fn Alert(
48 #[prop(optional, default = AlertVariant::Default)]
50 variant: AlertVariant,
51 #[prop(optional, default = AlertSize::Default)]
53 size: AlertSize,
54 #[prop(optional, default = false)]
56 _dismissible: bool,
57 #[prop(optional, default = true)]
59 visible: bool,
60 #[prop(optional)]
62 class: Option<String>,
63 #[prop(optional)]
65 style: Option<String>,
66 #[prop(optional)]
68 on_dismiss: Option<Callback<()>>,
69 children: Children,
71) -> impl IntoView {
72 let ___alert_id = generate_id("alert");
73
74 let data_variant = variant.as_str();
76 let data_size = size.as_str();
77
78 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 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 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#[component]
142pub fn AlertTitle(
143 #[prop(optional)]
145 class: Option<String>,
146 #[prop(optional)]
148 style: Option<String>,
149 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#[component]
168pub fn AlertDescription(
169 #[prop(optional)]
171 class: Option<String>,
172 #[prop(optional)]
174 style: Option<String>,
175 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 #[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 #[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 #[test]
275 fn test_alert_visibility_state() {
276 run_test(|| {
277 let mut visible = true;
278 let dismissible = true;
279
280 assert!(visible);
282 assert!(dismissible);
283
284 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 assert!(visible);
300 assert!(!dismissible);
301
302 dismissible = true;
304
305 assert!(visible);
306 assert!(dismissible);
307 });
308 }
309
310 #[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 #[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 #[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 let dismiss_clicked = true;
388 if dismiss_clicked && !dismissible {
389 }
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 if !visible {}
409 });
410 }
411
412 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 assert!(matches!(dismissible, true | false));
436 assert!(matches!(visible, true | false));
437
438 if !visible {
440 }
442
443 if !dismissible {
444 }
446 }
447 }
448
449 fn run_test<F>(f: F)
451 where
452 F: FnOnce(),
453 {
454 f();
455 }
456}