dioxus_ui_system/molecules/
dialog.rs1use crate::atoms::{Box, Button, ButtonVariant};
6use crate::styles::Style;
7use crate::theme::{use_style, use_theme};
8use dioxus::prelude::*;
9
10#[derive(Props, Clone, PartialEq)]
12pub struct DialogProps {
13 pub open: bool,
15 pub on_close: EventHandler<()>,
17 pub children: Element,
19 #[props(default)]
21 pub title: Option<String>,
22 #[props(default)]
24 pub description: Option<String>,
25 #[props(default = true)]
27 pub show_close_button: bool,
28 #[props(default = true)]
30 pub close_on_overlay_click: bool,
31 #[props(default)]
33 pub content_style: Option<String>,
34}
35
36#[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 let custom_content_style = props.content_style.clone().unwrap_or_default();
81
82 rsx! {
83 div {
84 style: "{overlay_style}",
85 onclick: handle_overlay_click,
86
87 div {
88 style: "{content_style} {custom_content_style}",
89 onclick: move |e| e.stop_propagation(),
90
91 if props.title.is_some() || props.show_close_button {
93 DialogHeader {
94 title: props.title.clone(),
95 show_close_button: props.show_close_button,
96 on_close: props.on_close.clone(),
97 }
98 }
99
100 if let Some(description) = props.description.clone() {
102 DialogDescription { description: description }
103 }
104
105 Box {
107 style: "padding: 0 24px 24px 24px; overflow-y: auto;",
108 {props.children}
109 }
110 }
111 }
112 }
113}
114
115use crate::theme::tokens::Color;
116
117#[derive(Props, Clone, PartialEq)]
118struct DialogHeaderProps {
119 title: Option<String>,
120 show_close_button: bool,
121 on_close: EventHandler<()>,
122}
123
124#[component]
125fn DialogHeader(props: DialogHeaderProps) -> Element {
126 let _theme = use_theme();
127
128 let header_style = use_style(|t| {
129 Style::new()
130 .flex()
131 .items_center()
132 .justify_between()
133 .p(&t.spacing, "lg")
134 .border_bottom(1, &t.colors.border)
135 .build()
136 });
137
138 rsx! {
139 div {
140 style: "{header_style}",
141
142 if let Some(title) = props.title {
143 h2 {
144 style: "margin: 0; font-size: 18px; font-weight: 600;",
145 "{title}"
146 }
147 } else {
148 div {}
149 }
150
151 if props.show_close_button {
152 Button {
153 variant: ButtonVariant::Ghost,
154 onclick: move |_| props.on_close.call(()),
155 "✕"
156 }
157 }
158 }
159 }
160}
161
162#[derive(Props, Clone, PartialEq)]
163struct DialogDescriptionProps {
164 description: String,
165}
166
167#[component]
168fn DialogDescription(props: DialogDescriptionProps) -> Element {
169 rsx! {
170 p {
171 style: "margin: 0; padding: 16px 24px 0 24px; font-size: 14px; color: #64748b;",
172 "{props.description}"
173 }
174 }
175}
176
177#[derive(Props, Clone, PartialEq)]
179pub struct DialogFooterProps {
180 pub children: Element,
182 #[props(default)]
184 pub align: DialogFooterAlign,
185}
186
187#[derive(Default, Clone, PartialEq)]
189pub enum DialogFooterAlign {
190 #[default]
192 Start,
193 Center,
195 End,
197 Between,
199}
200
201#[component]
203pub fn DialogFooter(props: DialogFooterProps) -> Element {
204 let _theme = use_theme();
205
206 let justify = match props.align {
207 DialogFooterAlign::Start => "flex-start",
208 DialogFooterAlign::Center => "center",
209 DialogFooterAlign::End => "flex-end",
210 DialogFooterAlign::Between => "space-between",
211 };
212
213 let footer_style = use_style(|t| {
214 Style::new()
215 .flex()
216 .items_center()
217 .gap(&t.spacing, "sm")
218 .p(&t.spacing, "lg")
219 .border_top(1, &t.colors.border)
220 .build()
221 });
222
223 rsx! {
224 div {
225 style: "{footer_style} justify-content: {justify};",
226 {props.children}
227 }
228 }
229}
230
231#[derive(Props, Clone, PartialEq)]
233pub struct AlertDialogProps {
234 pub open: bool,
236 pub on_close: EventHandler<()>,
238 pub title: String,
240 pub description: String,
242 #[props(default = "Cancel".to_string())]
244 pub cancel_text: String,
245 #[props(default = "Confirm".to_string())]
247 pub confirm_text: String,
248 pub on_confirm: EventHandler<()>,
250 #[props(default)]
252 pub destructive: bool,
253}
254
255#[component]
257pub fn AlertDialog(props: AlertDialogProps) -> Element {
258 let confirm_variant = if props.destructive {
259 ButtonVariant::Destructive
260 } else {
261 ButtonVariant::Primary
262 };
263
264 rsx! {
265 Dialog {
266 open: props.open,
267 on_close: props.on_close.clone(),
268 title: props.title.clone(),
269 show_close_button: false,
270 close_on_overlay_click: false,
271
272 p {
273 style: "margin: 0 0 24px 0; font-size: 14px; color: #64748b; line-height: 1.5;",
274 "{props.description}"
275 }
276
277 DialogFooter {
278 align: DialogFooterAlign::End,
279
280 Button {
281 variant: ButtonVariant::Ghost,
282 onclick: move |_| props.on_close.call(()),
283 "{props.cancel_text}"
284 }
285
286 Button {
287 variant: confirm_variant,
288 onclick: move |_| props.on_confirm.call(()),
289 "{props.confirm_text}"
290 }
291 }
292 }
293 }
294}