Skip to main content

dioxus_ui_system/molecules/
dialog.rs

1//! Dialog molecule component
2//!
3//! A window overlaid on either the primary window or another dialog window.
4
5use dioxus::prelude::*;
6use crate::theme::{use_theme, use_style};
7use crate::styles::Style;
8use crate::atoms::{Button, ButtonVariant};
9
10/// Dialog properties
11#[derive(Props, Clone, PartialEq)]
12pub struct DialogProps {
13    /// Whether the dialog is open
14    pub open: bool,
15    /// Callback when dialog should close
16    pub on_close: EventHandler<()>,
17    /// Dialog content
18    pub children: Element,
19    /// Dialog title
20    #[props(default)]
21    pub title: Option<String>,
22    /// Dialog description
23    #[props(default)]
24    pub description: Option<String>,
25    /// Whether to show close button
26    #[props(default = true)]
27    pub show_close_button: bool,
28    /// Whether clicking overlay closes the dialog
29    #[props(default = true)]
30    pub close_on_overlay_click: bool,
31    /// Custom inline styles for the content
32    #[props(default)]
33    pub content_style: Option<String>,
34}
35
36/// Dialog molecule component
37#[component]
38pub fn Dialog(props: DialogProps) -> Element {
39    let _theme = use_theme();
40    
41    if !props.open {
42        return rsx! {};
43    }
44    
45    let overlay_style = use_style(|_t| {
46        Style::new()
47            .fixed()
48            .top("0")
49            .left("0")
50            .w_full()
51            .h_full()
52            .bg(&Color::new_rgba(0, 0, 0, 0.5))
53            .z_index(100)
54            .flex()
55            .items_center()
56            .justify_center()
57            .build()
58    });
59    
60    let content_style = use_style(|t| {
61        Style::new()
62            .w_full()
63            .max_w_px(512)
64            .max_h_px(600)
65            .rounded(&t.radius, "lg")
66            .bg(&t.colors.background)
67            .shadow(&t.shadows.xl)
68            .overflow_hidden()
69            .flex()
70            .flex_col()
71            .build()
72    });
73    
74    let handle_overlay_click = move |_| {
75        if props.close_on_overlay_click {
76            props.on_close.call(());
77        }
78    };
79    
80    rsx! {
81        div {
82            style: "{overlay_style}",
83            onclick: handle_overlay_click,
84            
85            div {
86                style: "{content_style} {props.content_style.clone().unwrap_or_default()}",
87                onclick: move |e| e.stop_propagation(),
88                
89                // Header
90                if props.title.is_some() || props.show_close_button {
91                    DialogHeader {
92                        title: props.title.clone(),
93                        show_close_button: props.show_close_button,
94                        on_close: props.on_close.clone(),
95                    }
96                }
97                
98                // Description
99                if let Some(description) = props.description.clone() {
100                    DialogDescription { description: description }
101                }
102                
103                // Content
104                div {
105                    style: "padding: 0 24px 24px 24px; overflow-y: auto;",
106                    {props.children}
107                }
108            }
109        }
110    }
111}
112
113use crate::theme::tokens::Color;
114
115#[derive(Props, Clone, PartialEq)]
116struct DialogHeaderProps {
117    title: Option<String>,
118    show_close_button: bool,
119    on_close: EventHandler<()>,
120}
121
122#[component]
123fn DialogHeader(props: DialogHeaderProps) -> Element {
124    let _theme = use_theme();
125    
126    let header_style = use_style(|t| {
127        Style::new()
128            .flex()
129            .items_center()
130            .justify_between()
131            .p(&t.spacing, "lg")
132            .border_bottom(1, &t.colors.border)
133            .build()
134    });
135    
136    rsx! {
137        div {
138            style: "{header_style}",
139            
140            if let Some(title) = props.title {
141                h2 {
142                    style: "margin: 0; font-size: 18px; font-weight: 600;",
143                    "{title}"
144                }
145            } else {
146                div {}
147            }
148            
149            if props.show_close_button {
150                Button {
151                    variant: ButtonVariant::Ghost,
152                    onclick: move |_| props.on_close.call(()),
153                    "✕"
154                }
155            }
156        }
157    }
158}
159
160#[derive(Props, Clone, PartialEq)]
161struct DialogDescriptionProps {
162    description: String,
163}
164
165#[component]
166fn DialogDescription(props: DialogDescriptionProps) -> Element {
167    rsx! {
168        p {
169            style: "margin: 0; padding: 16px 24px 0 24px; font-size: 14px; color: #64748b;",
170            "{props.description}"
171        }
172    }
173}
174
175/// Dialog footer component for action buttons
176#[derive(Props, Clone, PartialEq)]
177pub struct DialogFooterProps {
178    /// Footer content (usually buttons)
179    pub children: Element,
180    /// Align content
181    #[props(default)]
182    pub align: DialogFooterAlign,
183}
184
185/// Dialog footer alignment
186#[derive(Default, Clone, PartialEq)]
187pub enum DialogFooterAlign {
188    /// Start alignment
189    #[default]
190    Start,
191    /// Center alignment
192    Center,
193    /// End alignment
194    End,
195    /// Space between
196    Between,
197}
198
199/// Dialog footer component
200#[component]
201pub fn DialogFooter(props: DialogFooterProps) -> Element {
202    let _theme = use_theme();
203    
204    let justify = match props.align {
205        DialogFooterAlign::Start => "flex-start",
206        DialogFooterAlign::Center => "center",
207        DialogFooterAlign::End => "flex-end",
208        DialogFooterAlign::Between => "space-between",
209    };
210    
211    let footer_style = use_style(|t| {
212        Style::new()
213            .flex()
214            .items_center()
215            .gap(&t.spacing, "sm")
216            .p(&t.spacing, "lg")
217            .border_top(1, &t.colors.border)
218            .build()
219    });
220    
221    rsx! {
222        div {
223            style: "{footer_style} justify-content: {justify};",
224            {props.children}
225        }
226    }
227}
228
229/// Alert dialog for important confirmations
230#[derive(Props, Clone, PartialEq)]
231pub struct AlertDialogProps {
232    /// Whether the dialog is open
233    pub open: bool,
234    /// Callback when dialog should close
235    pub on_close: EventHandler<()>,
236    /// Dialog title
237    pub title: String,
238    /// Dialog description
239    pub description: String,
240    /// Cancel button text
241    #[props(default = "Cancel".to_string())]
242    pub cancel_text: String,
243    /// Confirm button text
244    #[props(default = "Confirm".to_string())]
245    pub confirm_text: String,
246    /// Callback when confirmed
247    pub on_confirm: EventHandler<()>,
248    /// Whether the action is destructive
249    #[props(default)]
250    pub destructive: bool,
251}
252
253/// Alert dialog component
254#[component]
255pub fn AlertDialog(props: AlertDialogProps) -> Element {
256    let confirm_variant = if props.destructive {
257        ButtonVariant::Destructive
258    } else {
259        ButtonVariant::Primary
260    };
261    
262    rsx! {
263        Dialog {
264            open: props.open,
265            on_close: props.on_close.clone(),
266            title: props.title.clone(),
267            show_close_button: false,
268            close_on_overlay_click: false,
269            
270            p {
271                style: "margin: 0 0 24px 0; font-size: 14px; color: #64748b; line-height: 1.5;",
272                "{props.description}"
273            }
274            
275            DialogFooter {
276                align: DialogFooterAlign::End,
277                
278                Button {
279                    variant: ButtonVariant::Ghost,
280                    onclick: move |_| props.on_close.call(()),
281                    "{props.cancel_text}"
282                }
283                
284                Button {
285                    variant: confirm_variant,
286                    onclick: move |_| props.on_confirm.call(()),
287                    "{props.confirm_text}"
288                }
289            }
290        }
291    }
292}