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    /// Child elements (alternative to body prop for custom layout).
109    #[props(default)]
110    pub children: Element,
111}
112
113#[component]
114pub fn Modal(props: ModalProps) -> Element {
115    let is_shown = *props.show.read();
116    let mut show_signal = props.show;
117
118    if !is_shown {
119        return rsx! {};
120    }
121
122    let size_class = match props.size {
123        ModalSize::Sm => " modal-sm",
124        ModalSize::Default => "",
125        ModalSize::Lg => " modal-lg",
126        ModalSize::Xl => " modal-xl",
127    };
128
129    let centered = if props.centered {
130        " modal-dialog-centered"
131    } else {
132        ""
133    };
134
135    let scrollable = if props.scrollable {
136        " modal-dialog-scrollable"
137    } else {
138        ""
139    };
140
141    let fullscreen = match props.fullscreen {
142        ModalFullscreen::Off => "",
143        ModalFullscreen::Always => " modal-fullscreen",
144        ModalFullscreen::SmDown => " modal-fullscreen-sm-down",
145        ModalFullscreen::MdDown => " modal-fullscreen-md-down",
146        ModalFullscreen::LgDown => " modal-fullscreen-lg-down",
147        ModalFullscreen::XlDown => " modal-fullscreen-xl-down",
148        ModalFullscreen::XxlDown => " modal-fullscreen-xxl-down",
149    };
150
151    let dialog_class = if props.class.is_empty() {
152        format!("modal-dialog{size_class}{centered}{scrollable}{fullscreen}")
153    } else {
154        format!(
155            "modal-dialog{size_class}{centered}{scrollable}{fullscreen} {}",
156            props.class
157        )
158    };
159
160    let backdrop_close = props.backdrop_close;
161
162    rsx! {
163        // Backdrop
164        div {
165            class: "modal-backdrop fade show",
166            onclick: move |_| {
167                if backdrop_close {
168                    show_signal.set(false);
169                }
170            },
171        }
172        // Modal
173        div {
174            class: "modal fade show",
175            style: "display: block;",
176            tabindex: "-1",
177            role: "dialog",
178            "aria-modal": "true",
179            onclick: move |_| {
180                if backdrop_close {
181                    show_signal.set(false);
182                }
183            },
184            div {
185                class: "{dialog_class}",
186                // Stop click propagation so clicking inside the modal doesn't close it
187                onclick: move |evt| evt.stop_propagation(),
188                div { class: "modal-content",
189                    // Header
190                    if !props.title.is_empty() || props.show_close {
191                        div { class: "modal-header",
192                            if !props.title.is_empty() {
193                                h5 { class: "modal-title", "{props.title}" }
194                            }
195                            if props.show_close {
196                                button {
197                                    class: "btn-close",
198                                    r#type: "button",
199                                    "aria-label": "Close",
200                                    onclick: move |_| show_signal.set(false),
201                                }
202                            }
203                        }
204                    }
205                    // Body
206                    if let Some(body) = props.body {
207                        div { class: "modal-body", {body} }
208                    }
209                    {props.children}
210                    // Footer
211                    if let Some(footer) = props.footer {
212                        div { class: "modal-footer", {footer} }
213                    }
214                }
215            }
216        }
217    }
218}