Skip to main content

dioxus_bootstrap_css/
modal.rs

1use dioxus::prelude::*;
2
3use crate::types::ModalSize;
4
5/// Bootstrap Modal component — signal-driven, no JavaScript.
6///
7/// ```rust
8/// let show = use_signal(|| false);
9/// rsx! {
10///     Button { onclick: move |_| show.set(true), "Open Modal" }
11///     Modal {
12///         show: show,
13///         title: "Confirm Action",
14///         body: rsx! { p { "Are you sure you want to proceed?" } },
15///         footer: rsx! {
16///             Button { color: Color::Secondary, onclick: move |_| show.set(false), "Cancel" }
17///             Button { color: Color::Primary, "Confirm" }
18///         },
19///     }
20/// }
21/// ```
22#[derive(Clone, PartialEq, Props)]
23pub struct ModalProps {
24    /// Signal controlling modal visibility.
25    pub show: Signal<bool>,
26    /// Modal title.
27    #[props(default)]
28    pub title: String,
29    /// Modal body content.
30    #[props(default)]
31    pub body: Option<Element>,
32    /// Modal footer content.
33    #[props(default)]
34    pub footer: Option<Element>,
35    /// Modal size.
36    #[props(default)]
37    pub size: ModalSize,
38    /// Close when clicking the backdrop.
39    #[props(default = true)]
40    pub backdrop_close: bool,
41    /// Show the close button in the header.
42    #[props(default = true)]
43    pub show_close: bool,
44    /// Center the modal vertically.
45    #[props(default)]
46    pub centered: bool,
47    /// Allow the modal body to scroll.
48    #[props(default)]
49    pub scrollable: bool,
50    /// Additional CSS classes for the modal-dialog.
51    #[props(default)]
52    pub class: String,
53    /// Child elements (alternative to body prop for custom layout).
54    #[props(default)]
55    pub children: Element,
56}
57
58#[component]
59pub fn Modal(props: ModalProps) -> Element {
60    let is_shown = *props.show.read();
61    let mut show_signal = props.show;
62
63    if !is_shown {
64        return rsx! {};
65    }
66
67    let size_class = match props.size {
68        ModalSize::Sm => " modal-sm",
69        ModalSize::Default => "",
70        ModalSize::Lg => " modal-lg",
71        ModalSize::Xl => " modal-xl",
72    };
73
74    let centered = if props.centered {
75        " modal-dialog-centered"
76    } else {
77        ""
78    };
79
80    let scrollable = if props.scrollable {
81        " modal-dialog-scrollable"
82    } else {
83        ""
84    };
85
86    let dialog_class = if props.class.is_empty() {
87        format!("modal-dialog{size_class}{centered}{scrollable}")
88    } else {
89        format!(
90            "modal-dialog{size_class}{centered}{scrollable} {}",
91            props.class
92        )
93    };
94
95    let backdrop_close = props.backdrop_close;
96
97    rsx! {
98        // Backdrop
99        div {
100            class: "modal-backdrop fade show",
101            onclick: move |_| {
102                if backdrop_close {
103                    show_signal.set(false);
104                }
105            },
106        }
107        // Modal
108        div {
109            class: "modal fade show",
110            style: "display: block;",
111            tabindex: "-1",
112            role: "dialog",
113            "aria-modal": "true",
114            onclick: move |_| {
115                if backdrop_close {
116                    show_signal.set(false);
117                }
118            },
119            div {
120                class: "{dialog_class}",
121                // Stop click propagation so clicking inside the modal doesn't close it
122                onclick: move |evt| evt.stop_propagation(),
123                div { class: "modal-content",
124                    // Header
125                    if !props.title.is_empty() || props.show_close {
126                        div { class: "modal-header",
127                            if !props.title.is_empty() {
128                                h5 { class: "modal-title", "{props.title}" }
129                            }
130                            if props.show_close {
131                                button {
132                                    class: "btn-close",
133                                    r#type: "button",
134                                    "aria-label": "Close",
135                                    onclick: move |_| show_signal.set(false),
136                                }
137                            }
138                        }
139                    }
140                    // Body
141                    if let Some(body) = props.body {
142                        div { class: "modal-body", {body} }
143                    }
144                    {props.children}
145                    // Footer
146                    if let Some(footer) = props.footer {
147                        div { class: "modal-footer", {footer} }
148                    }
149                }
150            }
151        }
152    }
153}