Skip to main content

dioxus_bootstrap_css/
toast.rs

1use dioxus::prelude::*;
2
3use crate::types::Color;
4
5/// Bootstrap Toast component — signal-driven, no JavaScript.
6///
7/// ```rust
8/// let show = use_signal(|| true);
9/// rsx! {
10///     Toast { show: show, title: "Notification",
11///         "You have a new message."
12///     }
13/// }
14/// ```
15#[derive(Clone, PartialEq, Props)]
16pub struct ToastProps {
17    /// Signal controlling visibility.
18    pub show: Signal<bool>,
19    /// Toast title (shown in header).
20    #[props(default)]
21    pub title: String,
22    /// Small text in header (e.g., "just now", "2 mins ago").
23    #[props(default)]
24    pub subtitle: String,
25    /// Show close button.
26    #[props(default = true)]
27    pub show_close: bool,
28    /// Toast color variant (applied as bg class).
29    #[props(default)]
30    pub color: Option<Color>,
31    /// Additional CSS classes.
32    #[props(default)]
33    pub class: String,
34    /// Toast body content.
35    pub children: Element,
36}
37
38#[component]
39pub fn Toast(props: ToastProps) -> Element {
40    let is_shown = *props.show.read();
41    let mut show_signal = props.show;
42
43    if !is_shown {
44        return rsx! {};
45    }
46
47    let color_class = match &props.color {
48        Some(c) => format!(" text-bg-{c}"),
49        None => String::new(),
50    };
51
52    let full_class = if props.class.is_empty() {
53        format!("toast show{color_class}")
54    } else {
55        format!("toast show{color_class} {}", props.class)
56    };
57
58    rsx! {
59        div {
60            class: "{full_class}",
61            role: "alert",
62            "aria-live": "assertive",
63            "aria-atomic": "true",
64            if !props.title.is_empty() {
65                div { class: "toast-header",
66                    strong { class: "me-auto", "{props.title}" }
67                    if !props.subtitle.is_empty() {
68                        small { "{props.subtitle}" }
69                    }
70                    if props.show_close {
71                        button {
72                            class: "btn-close",
73                            r#type: "button",
74                            "aria-label": "Close",
75                            onclick: move |_| show_signal.set(false),
76                        }
77                    }
78                }
79            }
80            div { class: "toast-body", {props.children} }
81        }
82    }
83}
84
85/// Container for positioning toasts on screen.
86///
87/// ```rust
88/// rsx! {
89///     ToastContainer { position: ToastPosition::TopEnd,
90///         Toast { show: signal1, title: "Success", "Saved!" }
91///         Toast { show: signal2, title: "Error", color: Color::Danger, "Failed." }
92///     }
93/// }
94/// ```
95#[derive(Clone, PartialEq, Props)]
96pub struct ToastContainerProps {
97    /// Position on screen.
98    #[props(default)]
99    pub position: ToastPosition,
100    /// Additional CSS classes.
101    #[props(default)]
102    pub class: String,
103    /// Child elements (Toast components).
104    pub children: Element,
105}
106
107/// Toast position on screen.
108#[derive(Clone, Copy, Debug, Default, PartialEq)]
109pub enum ToastPosition {
110    TopStart,
111    TopCenter,
112    #[default]
113    TopEnd,
114    MiddleCenter,
115    BottomStart,
116    BottomCenter,
117    BottomEnd,
118}
119
120impl std::fmt::Display for ToastPosition {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        match self {
123            ToastPosition::TopStart => write!(f, "top-0 start-0"),
124            ToastPosition::TopCenter => write!(f, "top-0 start-50 translate-middle-x"),
125            ToastPosition::TopEnd => write!(f, "top-0 end-0"),
126            ToastPosition::MiddleCenter => {
127                write!(f, "top-50 start-50 translate-middle")
128            }
129            ToastPosition::BottomStart => write!(f, "bottom-0 start-0"),
130            ToastPosition::BottomCenter => {
131                write!(f, "bottom-0 start-50 translate-middle-x")
132            }
133            ToastPosition::BottomEnd => write!(f, "bottom-0 end-0"),
134        }
135    }
136}
137
138#[component]
139pub fn ToastContainer(props: ToastContainerProps) -> Element {
140    let pos = props.position;
141    let full_class = if props.class.is_empty() {
142        format!("toast-container position-fixed p-3 {pos}")
143    } else {
144        format!("toast-container position-fixed p-3 {pos} {}", props.class)
145    };
146
147    rsx! {
148        div { class: "{full_class}", {props.children} }
149    }
150}