Skip to main content

stratum_components/overlay/
dialog.rs

1//! Styled Dialog component with overlay backdrop.
2
3use crate::common::merge_classes;
4use stratum_core::aria::{AriaAttributes, AriaRole};
5use stratum_core::render::{AttrValue, RenderOutput};
6
7/// Properties for the Dialog overlay/backdrop.
8#[derive(Debug, Clone, PartialEq, Default)]
9pub struct DialogOverlayProps {
10    pub class: Option<String>,
11}
12
13pub struct DialogOverlay;
14
15impl DialogOverlay {
16    const BASE: &'static str = "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0";
17
18    pub fn classes(props: &DialogOverlayProps) -> String {
19        merge_classes(Self::BASE, &props.class)
20    }
21
22    pub fn render(props: &DialogOverlayProps) -> RenderOutput {
23        RenderOutput::new()
24            .with_tag("div")
25            .with_class(Self::classes(props))
26            .with_aria({
27                let mut a = AriaAttributes::new();
28                a.hidden = Some(true);
29                a
30            })
31    }
32}
33
34/// Properties for the Dialog content panel.
35#[derive(Debug, Clone, PartialEq, Default)]
36pub struct DialogProps {
37    pub open: bool,
38    pub class: Option<String>,
39    pub aria_label: Option<String>,
40    pub aria_describedby: Option<String>,
41    pub id: Option<String>,
42}
43
44pub struct Dialog;
45
46impl Dialog {
47    const BASE: &'static str = "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg";
48
49    pub fn classes(props: &DialogProps) -> String {
50        merge_classes(Self::BASE, &props.class)
51    }
52
53    pub fn render(props: &DialogProps) -> RenderOutput {
54        let classes = Self::classes(props);
55        let mut aria = AriaAttributes::new()
56            .with_role(AriaRole::Dialog)
57            .with_modal(true);
58
59        if let Some(ref label) = props.aria_label {
60            aria = aria.with_label(label.clone());
61        }
62        if let Some(ref desc) = props.aria_describedby {
63            aria = aria.with_describedby(desc.clone());
64        }
65
66        let mut output = RenderOutput::new()
67            .with_tag("div")
68            .with_class(classes)
69            .with_aria(aria)
70            .with_data("state", if props.open { "open" } else { "closed" });
71
72        if let Some(ref id) = props.id {
73            output = output.with_attr("id", AttrValue::String(id.clone()));
74        }
75
76        output
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn dialog_overlay_classes() {
86        let props = DialogOverlayProps::default();
87        let classes = DialogOverlay::classes(&props);
88        assert!(classes.contains("fixed inset-0"));
89        assert!(classes.contains("bg-black/80"));
90    }
91
92    #[test]
93    fn dialog_render_open() {
94        let props = DialogProps {
95            open: true,
96            aria_label: Some("Confirm".to_string()),
97            ..Default::default()
98        };
99        let output = Dialog::render(&props);
100        assert_eq!(output.aria.role, Some(AriaRole::Dialog));
101        assert_eq!(output.aria.modal, Some(true));
102        assert!(
103            output
104                .data_attrs
105                .iter()
106                .any(|(k, v)| k == "state" && v == "open")
107        );
108    }
109
110    #[test]
111    fn dialog_render_closed() {
112        let props = DialogProps::default();
113        let output = Dialog::render(&props);
114        assert!(
115            output
116                .data_attrs
117                .iter()
118                .any(|(k, v)| k == "state" && v == "closed")
119        );
120    }
121}