Skip to main content

dioxus_bootstrap_css/
toast.rs

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