Skip to main content

dioxus_bootstrap_css/
modal.rs

1use dioxus::prelude::*;
2
3use crate::types::ModalSize;
4
5/// Modal fullscreen variants.
6#[derive(Clone, Copy, Debug, Default, PartialEq)]
7pub enum ModalFullscreen {
8    /// Not fullscreen.
9    #[default]
10    Off,
11    /// Always fullscreen.
12    Always,
13    /// Fullscreen below sm breakpoint.
14    SmDown,
15    /// Fullscreen below md breakpoint.
16    MdDown,
17    /// Fullscreen below lg breakpoint.
18    LgDown,
19    /// Fullscreen below xl breakpoint.
20    XlDown,
21    /// Fullscreen below xxl breakpoint.
22    XxlDown,
23}
24
25/// Bootstrap Modal component — signal-driven, no JavaScript.
26///
27/// Replaces Bootstrap's `<div class="modal">` + JavaScript with a signal-controlled component.
28///
29/// # Bootstrap HTML → Dioxus
30///
31/// ```html
32/// <!-- Bootstrap HTML (requires JavaScript) -->
33/// <div class="modal fade" tabindex="-1">
34///   <div class="modal-dialog modal-lg modal-dialog-centered">
35///     <div class="modal-content">
36///       <div class="modal-header"><h5 class="modal-title">Title</h5></div>
37///       <div class="modal-body"><p>Body</p></div>
38///       <div class="modal-footer"><button class="btn btn-primary">OK</button></div>
39///     </div>
40///   </div>
41/// </div>
42/// ```
43///
44/// ```rust,no_run
45/// // Dioxus equivalent — no JavaScript needed
46/// let show = use_signal(|| false);
47/// rsx! {
48///     Button { onclick: move |_| show.set(true), "Open Modal" }
49///     Modal {
50///         show: show,
51///         title: "Confirm Action",
52///         size: ModalSize::Lg,
53///         centered: true,
54///         body: rsx! { p { "Are you sure?" } },
55///         footer: rsx! {
56///             Button { color: Color::Secondary, onclick: move |_| show.set(false), "Cancel" }
57///             Button { color: Color::Primary, "Confirm" }
58///         },
59///     }
60/// }
61/// ```
62///
63/// # Props
64///
65/// - `show` — `Signal<bool>` controlling visibility
66/// - `title` — modal title text
67/// - `body` — modal body content (Element)
68/// - `footer` — modal footer content (Element)
69/// - `size` — `ModalSize::Sm`, `Default`, `Lg`, `Xl`
70/// - `fullscreen` — `ModalFullscreen::Off`, `Always`, `SmDown`..`XxlDown`
71/// - `centered` — vertically center the modal
72/// - `scrollable` — scrollable modal body
73/// - `backdrop_close` — close when clicking backdrop (default: true)
74#[derive(Clone, PartialEq, Props)]
75pub struct ModalProps {
76    /// Signal controlling modal visibility.
77    pub show: Signal<bool>,
78    /// Modal title.
79    #[props(default)]
80    pub title: String,
81    /// Modal body content.
82    #[props(default)]
83    pub body: Option<Element>,
84    /// Modal footer content.
85    #[props(default)]
86    pub footer: Option<Element>,
87    /// Modal size.
88    #[props(default)]
89    pub size: ModalSize,
90    /// Close when clicking the backdrop.
91    #[props(default = true)]
92    pub backdrop_close: bool,
93    /// Show the close button in the header.
94    #[props(default = true)]
95    pub show_close: bool,
96    /// Center the modal vertically.
97    #[props(default)]
98    pub centered: bool,
99    /// Allow the modal body to scroll.
100    #[props(default)]
101    pub scrollable: bool,
102    /// Fullscreen mode.
103    #[props(default)]
104    pub fullscreen: ModalFullscreen,
105    /// Additional CSS classes for the modal-dialog.
106    #[props(default)]
107    pub class: String,
108    /// Any additional HTML attributes.
109    #[props(extends = GlobalAttributes)]
110    attributes: Vec<Attribute>,
111    /// Child elements (alternative to body prop for custom layout).
112    #[props(default)]
113    pub children: Element,
114}
115
116#[component]
117pub fn Modal(props: ModalProps) -> Element {
118    let is_shown = *props.show.read();
119    let mut show_signal = props.show;
120
121    if !is_shown {
122        return rsx! {};
123    }
124
125    let size_class = match props.size {
126        ModalSize::Sm => " modal-sm",
127        ModalSize::Default => "",
128        ModalSize::Lg => " modal-lg",
129        ModalSize::Xl => " modal-xl",
130    };
131
132    let centered = if props.centered {
133        " modal-dialog-centered"
134    } else {
135        ""
136    };
137
138    let scrollable = if props.scrollable {
139        " modal-dialog-scrollable"
140    } else {
141        ""
142    };
143
144    let fullscreen = match props.fullscreen {
145        ModalFullscreen::Off => "",
146        ModalFullscreen::Always => " modal-fullscreen",
147        ModalFullscreen::SmDown => " modal-fullscreen-sm-down",
148        ModalFullscreen::MdDown => " modal-fullscreen-md-down",
149        ModalFullscreen::LgDown => " modal-fullscreen-lg-down",
150        ModalFullscreen::XlDown => " modal-fullscreen-xl-down",
151        ModalFullscreen::XxlDown => " modal-fullscreen-xxl-down",
152    };
153
154    let dialog_class = if props.class.is_empty() {
155        format!("modal-dialog{size_class}{centered}{scrollable}{fullscreen}")
156    } else {
157        format!(
158            "modal-dialog{size_class}{centered}{scrollable}{fullscreen} {}",
159            props.class
160        )
161    };
162
163    let backdrop_close = props.backdrop_close;
164
165    rsx! {
166        // Backdrop
167        div {
168            class: "modal-backdrop fade show",
169            onclick: move |_| {
170                if backdrop_close {
171                    show_signal.set(false);
172                }
173            },
174        }
175        // Modal
176        div {
177            class: "modal fade show",
178            style: "display: block;",
179            tabindex: "-1",
180            role: "dialog",
181            "aria-modal": "true",
182            onclick: move |_| {
183                if backdrop_close {
184                    show_signal.set(false);
185                }
186            },
187            ..props.attributes,
188            div {
189                class: "{dialog_class}",
190                // Stop click propagation so clicking inside the modal doesn't close it
191                onclick: move |evt| evt.stop_propagation(),
192                div { class: "modal-content",
193                    // Header
194                    if !props.title.is_empty() || props.show_close {
195                        div { class: "modal-header",
196                            if !props.title.is_empty() {
197                                h5 { class: "modal-title", "{props.title}" }
198                            }
199                            if props.show_close {
200                                button {
201                                    class: "btn-close",
202                                    r#type: "button",
203                                    "aria-label": "Close",
204                                    onclick: move |_| show_signal.set(false),
205                                }
206                            }
207                        }
208                    }
209                    // Body
210                    if let Some(body) = props.body {
211                        div { class: "modal-body", {body} }
212                    }
213                    {props.children}
214                    // Footer
215                    if let Some(footer) = props.footer {
216                        div { class: "modal-footer", {footer} }
217                    }
218                }
219            }
220        }
221    }
222}