ironhtml_bootstrap/
toast.rs

1//! Bootstrap toast components.
2//!
3//! Provides type-safe Bootstrap toasts matching the
4//! [Bootstrap toast documentation](https://getbootstrap.com/docs/5.3/components/toasts/).
5
6use ironhtml::typed::Element;
7use ironhtml_elements::{Button, Div, Img, Small, Strong};
8
9extern crate alloc;
10use alloc::format;
11use alloc::string::ToString;
12
13/// Create a basic Bootstrap toast.
14///
15/// ## Example
16///
17/// ```rust
18/// use ironhtml_bootstrap::toast::toast;
19///
20/// let t = toast("Hello, world!", "11 mins ago");
21/// assert!(t.render().contains("toast"));
22/// ```
23#[must_use]
24pub fn toast(message: &str, time: &str) -> Element<Div> {
25    Element::<Div>::new()
26        .class("toast")
27        .attr("role", "alert")
28        .attr("aria-live", "assertive")
29        .attr("aria-atomic", "true")
30        .child::<Div, _>(|header| {
31            header
32                .class("toast-header")
33                .child::<Strong, _>(|s| s.class("me-auto").text("Bootstrap"))
34                .child::<Small, _>(|s| s.text(time))
35                .child::<Button, _>(|b| {
36                    b.attr("type", "button")
37                        .class("btn-close")
38                        .attr("data-bs-dismiss", "toast")
39                        .attr("aria-label", "Close")
40                })
41        })
42        .child::<Div, _>(|body| body.class("toast-body").text(message))
43}
44
45/// Create a toast with custom header title.
46#[must_use]
47pub fn toast_titled(title: &str, message: &str, time: &str) -> Element<Div> {
48    Element::<Div>::new()
49        .class("toast")
50        .attr("role", "alert")
51        .attr("aria-live", "assertive")
52        .attr("aria-atomic", "true")
53        .child::<Div, _>(|header| {
54            header
55                .class("toast-header")
56                .child::<Strong, _>(|s| s.class("me-auto").text(title))
57                .child::<Small, _>(|s| s.class("text-body-secondary").text(time))
58                .child::<Button, _>(|b| {
59                    b.attr("type", "button")
60                        .class("btn-close")
61                        .attr("data-bs-dismiss", "toast")
62                        .attr("aria-label", "Close")
63                })
64        })
65        .child::<Div, _>(|body| body.class("toast-body").text(message))
66}
67
68/// Create a toast with image in header.
69#[must_use]
70pub fn toast_with_image(
71    img_src: &str,
72    img_alt: &str,
73    title: &str,
74    message: &str,
75    time: &str,
76) -> Element<Div> {
77    Element::<Div>::new()
78        .class("toast")
79        .attr("role", "alert")
80        .attr("aria-live", "assertive")
81        .attr("aria-atomic", "true")
82        .child::<Div, _>(|header| {
83            header
84                .class("toast-header")
85                .child::<Img, _>(|i| {
86                    i.attr("src", img_src)
87                        .attr("alt", img_alt)
88                        .class("rounded me-2")
89                        .attr("style", "width: 20px; height: 20px;")
90                })
91                .child::<Strong, _>(|s| s.class("me-auto").text(title))
92                .child::<Small, _>(|s| s.text(time))
93                .child::<Button, _>(|b| {
94                    b.attr("type", "button")
95                        .class("btn-close")
96                        .attr("data-bs-dismiss", "toast")
97                        .attr("aria-label", "Close")
98                })
99        })
100        .child::<Div, _>(|body| body.class("toast-body").text(message))
101}
102
103/// Create a simple toast without header.
104#[must_use]
105pub fn toast_simple(message: &str) -> Element<Div> {
106    Element::<Div>::new()
107        .class("toast align-items-center")
108        .attr("role", "alert")
109        .attr("aria-live", "assertive")
110        .attr("aria-atomic", "true")
111        .child::<Div, _>(|d| {
112            d.class("d-flex")
113                .child::<Div, _>(|body| body.class("toast-body").text(message))
114                .child::<Button, _>(|b| {
115                    b.attr("type", "button")
116                        .class("btn-close me-2 m-auto")
117                        .attr("data-bs-dismiss", "toast")
118                        .attr("aria-label", "Close")
119                })
120        })
121}
122
123/// Create a colored toast.
124#[must_use]
125pub fn toast_colored(color: crate::Color, message: &str) -> Element<Div> {
126    let class = format!(
127        "toast align-items-center text-bg-{} border-0",
128        color.as_str()
129    );
130
131    Element::<Div>::new()
132        .class(&class)
133        .attr("role", "alert")
134        .attr("aria-live", "assertive")
135        .attr("aria-atomic", "true")
136        .child::<Div, _>(|d| {
137            d.class("d-flex")
138                .child::<Div, _>(|body| body.class("toast-body").text(message))
139                .child::<Button, _>(|b| {
140                    let btn_class = if matches!(color, crate::Color::Light) {
141                        "btn-close me-2 m-auto"
142                    } else {
143                        "btn-close btn-close-white me-2 m-auto"
144                    };
145                    b.attr("type", "button")
146                        .class(btn_class)
147                        .attr("data-bs-dismiss", "toast")
148                        .attr("aria-label", "Close")
149                })
150        })
151}
152
153/// Create a toast container for stacking multiple toasts.
154///
155/// Position classes: top-0/bottom-0, start-0/end-0, translate-middle
156#[must_use]
157pub fn toast_container<F>(position_class: &str, f: F) -> Element<Div>
158where
159    F: FnOnce(Element<Div>) -> Element<Div>,
160{
161    let class = format!("toast-container position-fixed p-3 {position_class}");
162    f(Element::<Div>::new().class(&class))
163}
164
165/// Create a toast that auto-hides after delay.
166#[must_use]
167pub fn toast_autohide(message: &str, delay_ms: u32) -> Element<Div> {
168    Element::<Div>::new()
169        .class("toast")
170        .attr("role", "alert")
171        .attr("aria-live", "assertive")
172        .attr("aria-atomic", "true")
173        .attr("data-bs-autohide", "true")
174        .attr("data-bs-delay", delay_ms.to_string())
175        .child::<Div, _>(|header| {
176            header
177                .class("toast-header")
178                .child::<Strong, _>(|s| s.class("me-auto").text("Notification"))
179                .child::<Button, _>(|b| {
180                    b.attr("type", "button")
181                        .class("btn-close")
182                        .attr("data-bs-dismiss", "toast")
183                        .attr("aria-label", "Close")
184                })
185        })
186        .child::<Div, _>(|body| body.class("toast-body").text(message))
187}
188
189/// Create a toast that is shown by default.
190#[must_use]
191pub fn toast_show(message: &str, time: &str) -> Element<Div> {
192    Element::<Div>::new()
193        .class("toast show")
194        .attr("role", "alert")
195        .attr("aria-live", "assertive")
196        .attr("aria-atomic", "true")
197        .child::<Div, _>(|header| {
198            header
199                .class("toast-header")
200                .child::<Strong, _>(|s| s.class("me-auto").text("Bootstrap"))
201                .child::<Small, _>(|s| s.text(time))
202                .child::<Button, _>(|b| {
203                    b.attr("type", "button")
204                        .class("btn-close")
205                        .attr("data-bs-dismiss", "toast")
206                        .attr("aria-label", "Close")
207                })
208        })
209        .child::<Div, _>(|body| body.class("toast-body").text(message))
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_toast() {
218        let t = toast("Hello!", "just now");
219        let html = t.render();
220        assert!(html.contains("toast"));
221        assert!(html.contains("toast-header"));
222        assert!(html.contains("toast-body"));
223        assert!(html.contains("Hello!"));
224    }
225
226    #[test]
227    fn test_toast_colored() {
228        let t = toast_colored(crate::Color::Success, "Success!");
229        let html = t.render();
230        assert!(html.contains("text-bg-success"));
231        assert!(html.contains("btn-close-white"));
232    }
233
234    #[test]
235    fn test_toast_simple() {
236        let t = toast_simple("Simple message");
237        let html = t.render();
238        assert!(html.contains("toast"));
239        assert!(!html.contains("toast-header"));
240    }
241
242    #[test]
243    fn test_toast_container() {
244        let container = toast_container("top-0 end-0", |c| {
245            c.child::<Div, _>(|_| toast_show("Message 1", "now"))
246                .child::<Div, _>(|_| toast_show("Message 2", "now"))
247        });
248        let html = container.render();
249        assert!(html.contains("toast-container"));
250        assert!(html.contains("position-fixed"));
251    }
252}