1use ironhtml::typed::Element;
7use ironhtml_elements::{Button, Div, A, H5};
8
9extern crate alloc;
10use alloc::format;
11
12#[derive(Clone, Copy, Default)]
14pub enum OffcanvasPlacement {
15 #[default]
16 Start,
17 End,
18 Top,
19 Bottom,
20}
21
22impl OffcanvasPlacement {
23 const fn class(self) -> &'static str {
24 match self {
25 Self::Start => "offcanvas-start",
26 Self::End => "offcanvas-end",
27 Self::Top => "offcanvas-top",
28 Self::Bottom => "offcanvas-bottom",
29 }
30 }
31}
32
33#[must_use]
45pub fn offcanvas_button(target_id: &str, color: crate::Color, text: &str) -> Element<Button> {
46 let class = format!("btn btn-{}", color.as_str());
47 Element::<Button>::new()
48 .class(&class)
49 .attr("type", "button")
50 .attr("data-bs-toggle", "offcanvas")
51 .attr("data-bs-target", format!("#{target_id}"))
52 .attr("aria-controls", target_id)
53 .text(text)
54}
55
56#[must_use]
58pub fn offcanvas_link(target_id: &str, text: &str) -> Element<A> {
59 Element::<A>::new()
60 .class("btn btn-primary")
61 .attr("data-bs-toggle", "offcanvas")
62 .attr("href", format!("#{target_id}"))
63 .attr("role", "button")
64 .attr("aria-controls", target_id)
65 .text(text)
66}
67
68#[must_use]
81pub fn offcanvas<F>(
82 id: &str,
83 placement: OffcanvasPlacement,
84 title: &str,
85 body_fn: F,
86) -> Element<Div>
87where
88 F: FnOnce(Element<Div>) -> Element<Div>,
89{
90 let class = format!("offcanvas {}", placement.class());
91
92 Element::<Div>::new()
93 .class(&class)
94 .attr("tabindex", "-1")
95 .attr("id", id)
96 .attr("aria-labelledby", format!("{id}Label"))
97 .child::<Div, _>(|header| {
99 header
100 .class("offcanvas-header")
101 .child::<H5, _>(|h| {
102 h.class("offcanvas-title")
103 .attr("id", format!("{id}Label"))
104 .text(title)
105 })
106 .child::<Button, _>(|b| {
107 b.attr("type", "button")
108 .class("btn-close")
109 .attr("data-bs-dismiss", "offcanvas")
110 .attr("aria-label", "Close")
111 })
112 })
113 .child::<Div, _>(|body| body_fn(body.class("offcanvas-body")))
115}
116
117#[must_use]
119pub fn offcanvas_static<F>(
120 id: &str,
121 placement: OffcanvasPlacement,
122 title: &str,
123 body_fn: F,
124) -> Element<Div>
125where
126 F: FnOnce(Element<Div>) -> Element<Div>,
127{
128 let class = format!("offcanvas {}", placement.class());
129
130 Element::<Div>::new()
131 .class(&class)
132 .attr("data-bs-backdrop", "static")
133 .attr("tabindex", "-1")
134 .attr("id", id)
135 .attr("aria-labelledby", format!("{id}Label"))
136 .child::<Div, _>(|header| {
137 header
138 .class("offcanvas-header")
139 .child::<H5, _>(|h| {
140 h.class("offcanvas-title")
141 .attr("id", format!("{id}Label"))
142 .text(title)
143 })
144 .child::<Button, _>(|b| {
145 b.attr("type", "button")
146 .class("btn-close")
147 .attr("data-bs-dismiss", "offcanvas")
148 .attr("aria-label", "Close")
149 })
150 })
151 .child::<Div, _>(|body| body_fn(body.class("offcanvas-body")))
152}
153
154#[must_use]
156pub fn offcanvas_scroll<F>(
157 id: &str,
158 placement: OffcanvasPlacement,
159 title: &str,
160 body_fn: F,
161) -> Element<Div>
162where
163 F: FnOnce(Element<Div>) -> Element<Div>,
164{
165 let class = format!("offcanvas {}", placement.class());
166
167 Element::<Div>::new()
168 .class(&class)
169 .attr("data-bs-scroll", "true")
170 .attr("data-bs-backdrop", "false")
171 .attr("tabindex", "-1")
172 .attr("id", id)
173 .attr("aria-labelledby", format!("{id}Label"))
174 .child::<Div, _>(|header| {
175 header
176 .class("offcanvas-header")
177 .child::<H5, _>(|h| {
178 h.class("offcanvas-title")
179 .attr("id", format!("{id}Label"))
180 .text(title)
181 })
182 .child::<Button, _>(|b| {
183 b.attr("type", "button")
184 .class("btn-close")
185 .attr("data-bs-dismiss", "offcanvas")
186 .attr("aria-label", "Close")
187 })
188 })
189 .child::<Div, _>(|body| body_fn(body.class("offcanvas-body")))
190}
191
192#[must_use]
194pub fn offcanvas_dark<F>(
195 id: &str,
196 placement: OffcanvasPlacement,
197 title: &str,
198 body_fn: F,
199) -> Element<Div>
200where
201 F: FnOnce(Element<Div>) -> Element<Div>,
202{
203 let class = format!("offcanvas {} text-bg-dark", placement.class());
204
205 Element::<Div>::new()
206 .class(&class)
207 .attr("tabindex", "-1")
208 .attr("id", id)
209 .attr("aria-labelledby", format!("{id}Label"))
210 .child::<Div, _>(|header| {
211 header
212 .class("offcanvas-header")
213 .child::<H5, _>(|h| {
214 h.class("offcanvas-title")
215 .attr("id", format!("{id}Label"))
216 .text(title)
217 })
218 .child::<Button, _>(|b| {
219 b.attr("type", "button")
220 .class("btn-close btn-close-white")
221 .attr("data-bs-dismiss", "offcanvas")
222 .attr("aria-label", "Close")
223 })
224 })
225 .child::<Div, _>(|body| body_fn(body.class("offcanvas-body")))
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn test_offcanvas() {
234 let oc = offcanvas("test", OffcanvasPlacement::Start, "Title", |b| {
235 b.text("Content")
236 });
237 let html = oc.render();
238 assert!(html.contains("offcanvas"));
239 assert!(html.contains("offcanvas-start"));
240 assert!(html.contains("offcanvas-header"));
241 assert!(html.contains("offcanvas-body"));
242 }
243
244 #[test]
245 fn test_offcanvas_placements() {
246 let end = offcanvas("e", OffcanvasPlacement::End, "T", |b| b);
247 assert!(end.render().contains("offcanvas-end"));
248
249 let top = offcanvas("t", OffcanvasPlacement::Top, "T", |b| b);
250 assert!(top.render().contains("offcanvas-top"));
251
252 let bottom = offcanvas("b", OffcanvasPlacement::Bottom, "T", |b| b);
253 assert!(bottom.render().contains("offcanvas-bottom"));
254 }
255
256 #[test]
257 fn test_offcanvas_button() {
258 let btn = offcanvas_button("sidebar", crate::Color::Primary, "Open");
259 let html = btn.render();
260 assert!(html.contains(r#"data-bs-toggle="offcanvas"#));
261 assert!(html.contains(r##"data-bs-target="#sidebar""##));
262 }
263}