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}