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}