use dioxus::prelude::*;
use crate::types::Color;
#[derive(Clone, PartialEq, Props)]
pub struct ToastProps {
pub show: Signal<bool>,
#[props(default)]
pub title: String,
#[props(default)]
pub subtitle: String,
#[props(default = true)]
pub show_close: bool,
#[props(default)]
pub autohide: bool,
#[props(default = 5000)]
pub delay_ms: u32,
#[props(default)]
pub color: Option<Color>,
#[props(default)]
pub on_dismiss: Option<EventHandler<()>>,
#[props(default)]
pub class: String,
pub children: Element,
}
#[component]
pub fn Toast(props: ToastProps) -> Element {
let mut show_signal = props.show;
let on_dismiss = props.on_dismiss;
let autohide = props.autohide;
let delay_ms = props.delay_ms;
use_effect(move || {
let shown = *show_signal.read();
if autohide && shown {
spawn(async move {
gloo_timers::future::TimeoutFuture::new(delay_ms).await;
if *show_signal.peek() {
show_signal.set(false);
if let Some(handler) = &on_dismiss {
handler.call(());
}
}
});
}
});
let is_shown = *show_signal.read();
if !is_shown {
return rsx! {};
}
let dismiss = move |_| {
show_signal.set(false);
if let Some(handler) = &on_dismiss {
handler.call(());
}
};
let color_class = match &props.color {
Some(c) => format!(" text-bg-{c}"),
None => String::new(),
};
let full_class = if props.class.is_empty() {
format!("toast show{color_class}")
} else {
format!("toast show{color_class} {}", props.class)
};
let close_class = if props.color.is_some() {
"btn-close btn-close-white me-2 m-auto"
} else {
"btn-close"
};
rsx! {
div {
class: "{full_class}",
role: "alert",
"aria-live": "assertive",
"aria-atomic": "true",
if !props.title.is_empty() {
div { class: "toast-header",
strong { class: "me-auto", "{props.title}" }
if !props.subtitle.is_empty() {
small { "{props.subtitle}" }
}
if props.show_close {
button {
class: "btn-close",
r#type: "button",
"aria-label": "Close",
onclick: dismiss,
}
}
}
div { class: "toast-body", {props.children} }
} else if props.show_close {
div { class: "d-flex",
div { class: "toast-body", {props.children} }
button {
class: "{close_class}",
r#type: "button",
"aria-label": "Close",
onclick: move |_| show_signal.set(false),
}
}
} else {
div { class: "toast-body", {props.children} }
}
}
}
}
#[derive(Clone, PartialEq, Props)]
pub struct ToastContainerProps {
#[props(default)]
pub position: ToastPosition,
#[props(default = true)]
pub positioned: bool,
#[props(default)]
pub class: String,
pub children: Element,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum ToastPosition {
TopStart,
TopCenter,
#[default]
TopEnd,
MiddleCenter,
BottomStart,
BottomCenter,
BottomEnd,
}
impl std::fmt::Display for ToastPosition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ToastPosition::TopStart => write!(f, "top-0 start-0"),
ToastPosition::TopCenter => write!(f, "top-0 start-50 translate-middle-x"),
ToastPosition::TopEnd => write!(f, "top-0 end-0"),
ToastPosition::MiddleCenter => {
write!(f, "top-50 start-50 translate-middle")
}
ToastPosition::BottomStart => write!(f, "bottom-0 start-0"),
ToastPosition::BottomCenter => {
write!(f, "bottom-0 start-50 translate-middle-x")
}
ToastPosition::BottomEnd => write!(f, "bottom-0 end-0"),
}
}
}
fn toast_container_class(positioned: bool, position: ToastPosition, class: &str) -> String {
let placement = if positioned {
format!(" position-fixed p-3 {position}")
} else {
String::new()
};
if class.is_empty() {
format!("toast-container{placement}")
} else {
format!("toast-container{placement} {class}")
}
}
#[component]
pub fn ToastContainer(props: ToastContainerProps) -> Element {
let full_class = toast_container_class(props.positioned, props.position, &props.class);
rsx! {
div { class: "{full_class}", {props.children} }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn toast_container_is_positioned_by_default() {
assert_eq!(
toast_container_class(true, ToastPosition::default(), ""),
format!(
"toast-container position-fixed p-3 {}",
ToastPosition::default()
)
);
}
#[test]
fn toast_container_unpositioned_drops_the_utilities_entirely() {
let c = toast_container_class(false, ToastPosition::TopEnd, "");
assert_eq!(c, "toast-container");
assert!(!c.contains("position-fixed"));
}
#[test]
fn toast_container_extra_classes_survive_both_modes() {
assert!(toast_container_class(true, ToastPosition::TopEnd, "mt-5").ends_with(" mt-5"));
assert_eq!(
toast_container_class(false, ToastPosition::TopEnd, "mt-5"),
"toast-container mt-5"
);
}
}