use dioxus::prelude::*;
use crate::keyboard::is_escape_key;
use crate::types::ModalSize;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum ModalFullscreen {
#[default]
Off,
Always,
SmDown,
MdDown,
LgDown,
XlDown,
XxlDown,
}
#[derive(Clone, PartialEq, Props)]
pub struct ModalProps {
pub show: Signal<bool>,
#[props(default)]
pub title: String,
#[props(default)]
pub header: Option<Element>,
#[props(default)]
pub body: Option<Element>,
#[props(default)]
pub footer: Option<Element>,
#[props(default)]
pub size: ModalSize,
#[props(default = true)]
pub backdrop_close: bool,
#[props(default = true)]
pub keyboard_close: bool,
#[props(default = true)]
pub show_close: bool,
#[props(default)]
pub centered: bool,
#[props(default)]
pub scrollable: bool,
#[props(default)]
pub fullscreen: ModalFullscreen,
#[props(default)]
pub class: String,
#[props(default)]
pub content_class: String,
#[props(default)]
pub header_class: String,
#[props(default)]
pub body_class: String,
#[props(default)]
pub footer_class: String,
#[props(default)]
pub style: String,
#[props(default)]
pub backdrop_class: String,
#[props(default)]
pub backdrop_style: String,
#[props(default)]
pub on_dismiss: Option<EventHandler<()>>,
#[props(extends = GlobalAttributes)]
attributes: Vec<Attribute>,
#[props(default)]
pub children: Element,
}
fn modal_inline_style(extra: &str) -> String {
if extra.is_empty() {
"display: block;".to_string()
} else {
format!("display: block; {extra}")
}
}
#[component]
pub fn Modal(props: ModalProps) -> Element {
let is_shown = *props.show.read();
let mut show_signal = props.show;
if !is_shown {
return rsx! {};
}
let size_class = match props.size {
ModalSize::Sm => " modal-sm",
ModalSize::Default => "",
ModalSize::Lg => " modal-lg",
ModalSize::Xl => " modal-xl",
};
let centered = if props.centered {
" modal-dialog-centered"
} else {
""
};
let scrollable = if props.scrollable {
" modal-dialog-scrollable"
} else {
""
};
let fullscreen = match props.fullscreen {
ModalFullscreen::Off => "",
ModalFullscreen::Always => " modal-fullscreen",
ModalFullscreen::SmDown => " modal-fullscreen-sm-down",
ModalFullscreen::MdDown => " modal-fullscreen-md-down",
ModalFullscreen::LgDown => " modal-fullscreen-lg-down",
ModalFullscreen::XlDown => " modal-fullscreen-xl-down",
ModalFullscreen::XxlDown => " modal-fullscreen-xxl-down",
};
let dialog_class = if props.class.is_empty() {
format!("modal-dialog{size_class}{centered}{scrollable}{fullscreen}")
} else {
format!(
"modal-dialog{size_class}{centered}{scrollable}{fullscreen} {}",
props.class
)
};
let backdrop_close = props.backdrop_close;
let keyboard_close = props.keyboard_close;
let with_extra = |base: &str, extra: &str| {
if extra.is_empty() {
base.to_string()
} else {
format!("{base} {extra}")
}
};
let content_class = with_extra("modal-content", &props.content_class);
let header_class = with_extra("modal-header", &props.header_class);
let body_class = with_extra("modal-body", &props.body_class);
let footer_class = with_extra("modal-footer", &props.footer_class);
let on_dismiss = props.on_dismiss;
let mut dismiss = move || {
show_signal.set(false);
if let Some(handler) = &on_dismiss {
handler.call(());
}
};
let backdrop_full_class = with_extra("modal-backdrop fade show", &props.backdrop_class);
let modal_style = modal_inline_style(&props.style);
rsx! {
div {
class: "{backdrop_full_class}",
style: "{props.backdrop_style}",
onclick: move |_| {
if backdrop_close {
dismiss();
}
},
}
div {
class: "modal fade show",
style: "{modal_style}",
tabindex: "-1",
role: "dialog",
"aria-modal": "true",
onmounted: move |evt: MountedEvent| {
spawn(async move {
let _ = evt.set_focus(true).await;
});
},
onkeydown: move |evt: KeyboardEvent| {
if keyboard_close && is_escape_key(&evt.key()) {
dismiss();
}
},
onclick: move |_| {
if backdrop_close {
dismiss();
}
},
..props.attributes,
div {
class: "{dialog_class}",
onclick: move |evt| evt.stop_propagation(),
div { class: "{content_class}",
if props.header.is_some() || !props.title.is_empty() || props.show_close {
div { class: "{header_class}",
if let Some(header) = props.header {
{header}
} else if !props.title.is_empty() {
h5 { class: "modal-title", "{props.title}" }
}
if props.show_close {
button {
class: "btn-close",
r#type: "button",
"aria-label": "Close",
onclick: move |_| dismiss(),
}
}
}
}
if let Some(body) = props.body {
div { class: "{body_class}", {body} }
}
{props.children}
if let Some(footer) = props.footer {
div { class: "{footer_class}", {footer} }
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn modal_style_alone_is_the_display_override() {
assert_eq!(modal_inline_style(""), "display: block;");
}
#[test]
fn a_caller_style_is_appended_not_substituted() {
assert_eq!(
modal_inline_style("z-index: 1080;"),
"display: block; z-index: 1080;"
);
}
}