radix_leptos_primitives/components/
dialog.rs1use leptos::callback::Callback;
2use leptos::children::Children;
3use leptos::prelude::*;
4use wasm_bindgen::JsCast;
5use crate::utils::{merge_optional_classes, generate_id};
6
7#[derive(Debug, Clone, Copy, PartialEq)]
59pub enum DialogVariant {
60 Default,
61 Destructive,
62 Success,
63 Warning,
64 Info,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq)]
68pub enum DialogSize {
69 Default,
70 Sm,
71 Lg,
72 Xl,
73}
74
75impl DialogVariant {
76 pub fn as_str(&self) -> &'static str {
77 match self {
78 DialogVariant::Default => "default",
79 DialogVariant::Destructive => "destructive",
80 DialogVariant::Success => "success",
81 DialogVariant::Warning => "warning",
82 DialogVariant::Info => "info",
83 }
84 }
85}
86
87impl DialogSize {
88 pub fn as_str(&self) -> &'static str {
89 match self {
90 DialogSize::Default => "default",
91 DialogSize::Sm => "sm",
92 DialogSize::Lg => "lg",
93 DialogSize::Xl => "xl",
94 }
95 }
96}
97
98
99#[component]
101pub fn Dialog(
102 #[prop(optional, default = false)]
104 _open: bool,
105 #[prop(optional, default = DialogVariant::Default)]
107 variant: DialogVariant,
108 #[prop(optional, default = DialogSize::Default)]
110 size: DialogSize,
111 #[prop(optional)]
113 class: Option<String>,
114 #[prop(optional)]
116 style: Option<String>,
117 #[prop(optional)]
119 onopen_change: Option<Callback<bool>>,
120 children: Children,
122) -> impl IntoView {
123 let ___dialog_id = generate_id("dialog");
124 let title_id = generate_id("dialog-title");
125 let description_id = generate_id("dialog-description");
126
127 let data_variant = variant.as_str();
129 let data_size = size.as_str();
130
131 let base_classes = "radix-dialog";
133 let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
134 .unwrap_or_else(|| base_classes.to_string());
135
136 let handle_keydown = move |e: web_sys::KeyboardEvent| {
138 if e.key() == "Escape" {
139 if let Some(onopen_change) = onopen_change {
140 onopen_change.run(false);
141 }
142 }
143 };
144
145 let handle_backdrop_click = move |e: web_sys::MouseEvent| {
147 if let Some(target) = e.target() {
148 if let Ok(element) = target.dyn_into::<web_sys::Element>() {
149 if element.class_list().contains("radix-dialog-backdrop") {
150 if let Some(onopen_change) = onopen_change {
151 onopen_change.run(false);
152 }
153 }
154 }
155 }
156 };
157
158 view! {
159 <div
160 class=combined_class
161 style=style
162 data-variant=data_variant
163 data-size=data_size
164 on:keydown=handle_keydown
165 on:click=handle_backdrop_click
166 >
167 {children()}
168 </div>
169 }
170}
171
172#[component]
174pub fn DialogContent(
175 #[prop(optional)]
177 class: Option<String>,
178 #[prop(optional)]
180 style: Option<String>,
181 children: Children,
183) -> impl IntoView {
184 let base_classes = "radix-dialog-content";
185 let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
186 .unwrap_or_else(|| base_classes.to_string());
187
188 view! {
189 <div class=combined_class style=style>
190 {children()}
191 </div>
192 }
193}
194
195#[component]
197pub fn DialogHeader(
198 #[prop(optional)]
200 class: Option<String>,
201 #[prop(optional)]
203 style: Option<String>,
204 children: Children,
206) -> impl IntoView {
207 let base_classes = "radix-dialog-header";
208 let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
209 .unwrap_or_else(|| base_classes.to_string());
210
211 view! {
212 <div class=combined_class style=style>
213 {children()}
214 </div>
215 }
216}
217
218#[component]
220pub fn DialogTitle(
221 #[prop(optional)]
223 class: Option<String>,
224 #[prop(optional)]
226 style: Option<String>,
227 children: Children,
229) -> impl IntoView {
230 let base_classes = "radix-dialog-title";
231 let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
232 .unwrap_or_else(|| base_classes.to_string());
233
234 view! {
235 <h2 class=combined_class style=style>
236 {children()}
237 </h2>
238 }
239}
240
241#[component]
243pub fn DialogDescription(
244 #[prop(optional)]
246 class: Option<String>,
247 #[prop(optional)]
249 style: Option<String>,
250 children: Children,
252) -> impl IntoView {
253 let base_classes = "radix-dialog-description";
254 let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
255 .unwrap_or_else(|| base_classes.to_string());
256
257 view! {
258 <p class=combined_class style=style>
259 {children()}
260 </p>
261 }
262}
263
264#[component]
266pub fn DialogFooter(
267 #[prop(optional)]
269 class: Option<String>,
270 #[prop(optional)]
272 style: Option<String>,
273 children: Children,
275) -> impl IntoView {
276 let base_classes = "radix-dialog-footer";
277 let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
278 .unwrap_or_else(|| base_classes.to_string());
279
280 view! {
281 <div class=combined_class style=style>
282 {children()}
283 </div>
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use crate::{DialogSize, DialogVariant};
290 use proptest::prelude::*;
291use crate::utils::{merge_optional_classes, generate_id};
292
293 #[test]
295 fn test_dialog_variants() {
296 run_test(|| {
297 let variants = [DialogVariant::Default, DialogVariant::Destructive];
299
300 for variant in variants {
301 assert!(!variant.as_str().is_empty());
303 }
304 });
305 }
306
307 #[test]
308 fn test_dialog_sizes() {
309 run_test(|| {
310 let sizes = [
311 DialogSize::Default,
312 DialogSize::Sm,
313 DialogSize::Lg,
314 DialogSize::Xl,
315 ];
316
317 for size in sizes {
318 assert!(!size.as_str().is_empty());
320 }
321 });
322 }
323
324 #[test]
326 fn test_dialogopen_state() {
327 run_test(|| {
328 let open = true;
330 let variant = DialogVariant::Default;
331 let size = DialogSize::Default;
332
333 assert!(open);
335 assert_eq!(variant, DialogVariant::Default);
336 assert_eq!(size, DialogSize::Default);
337 });
338 }
339
340 #[test]
341 fn test_dialog_closed_state() {
342 run_test(|| {
343 let open = false;
345 let variant = DialogVariant::Destructive;
346 let size = DialogSize::Lg;
347
348 assert!(!open);
350 assert_eq!(variant, DialogVariant::Destructive);
351 assert_eq!(size, DialogSize::Lg);
352 });
353 }
354
355 #[test]
357 fn test_dialog_state_changes() {
358 run_test(|| {
359 let mut open = false;
361 let mut variant = DialogVariant::Default;
362 let mut size = DialogSize::Default;
363
364 assert!(!open);
366 assert_eq!(variant, DialogVariant::Default);
367 assert_eq!(size, DialogSize::Default);
368
369 open = true;
371 variant = DialogVariant::Destructive;
372 size = DialogSize::Lg;
373
374 assert!(open);
375 assert_eq!(variant, DialogVariant::Destructive);
376 assert_eq!(size, DialogSize::Lg);
377
378 open = false;
380
381 assert!(!open);
382 assert_eq!(variant, DialogVariant::Destructive);
383 assert_eq!(size, DialogSize::Lg);
384 });
385 }
386
387 #[test]
389 fn test_dialog_escape_key() {
390 run_test(|| {
391 let mut open = true;
393 let escape_pressed = true;
394
395 assert!(open);
397 assert!(escape_pressed);
398
399 if escape_pressed {
401 open = false;
402 }
403
404 assert!(!open);
405 });
406 }
407
408 #[test]
409 fn test_dialog_backdrop_click() {
410 run_test(|| {
411 let mut open = true;
413 let backdrop_clicked = true;
414
415 assert!(open);
417 assert!(backdrop_clicked);
418
419 if backdrop_clicked {
421 open = false;
422 }
423
424 assert!(!open);
425 });
426 }
427
428 #[test]
430 fn test_dialog_accessibility() {
431 run_test(|| {
432 let open = true;
434 let role = "dialog";
435 let aria_modal = "true";
436 let tabindex = "-1";
437
438 assert!(open);
440 assert_eq!(role, "dialog");
441 assert_eq!(aria_modal, "true");
442 assert_eq!(tabindex, "-1");
443 });
444 }
445
446 #[test]
448 fn test_dialog_edge_cases() {
449 run_test(|| {
450 let open = true;
452 let has_content = false;
453
454 assert!(open);
456 assert!(!has_content);
457 });
458 }
459
460 proptest! {
462 #[test]
463 fn test_dialog_properties(
464 variant in prop::sample::select(&[
465 DialogVariant::Default,
466 DialogVariant::Destructive,
467 ]),
468 size in prop::sample::select(&[
469 DialogSize::Default,
470 DialogSize::Sm,
471 DialogSize::Lg,
472 DialogSize::Xl,
473 ]),
474 open in prop::bool::ANY
475 ) {
476 assert!(!variant.as_str().is_empty());
479 assert!(!size.as_str().is_empty());
480
481 assert!(matches!(open, true | false));
483
484 match size {
486 DialogSize::Default => assert_eq!(size.as_str(), "default"),
487 DialogSize::Sm => assert_eq!(size.as_str(), "sm"),
488 DialogSize::Lg => assert_eq!(size.as_str(), "lg"),
489 DialogSize::Xl => assert_eq!(size.as_str(), "xl"),
490 }
491 }
492 }
493
494 fn run_test<F>(f: F)
496 where
497 F: FnOnce(),
498 {
499 f();
501 }
502}