1use crate::Color;
7use ironhtml::typed::Element;
8use ironhtml_elements::{Button, Div, Hr, Li, Span, Ul, A};
9
10extern crate alloc;
11use alloc::format;
12use alloc::string::String;
13
14pub enum DropdownItem {
16 Link { text: String, href: String },
18 Active { text: String, href: String },
20 Disabled { text: String, href: String },
22 Divider,
24 Header(String),
26 Text(String),
28}
29
30impl DropdownItem {
31 #[must_use]
33 pub fn link(text: impl Into<String>, href: impl Into<String>) -> Self {
34 Self::Link {
35 text: text.into(),
36 href: href.into(),
37 }
38 }
39
40 #[must_use]
42 pub fn active(text: impl Into<String>, href: impl Into<String>) -> Self {
43 Self::Active {
44 text: text.into(),
45 href: href.into(),
46 }
47 }
48
49 #[must_use]
51 pub fn disabled(text: impl Into<String>, href: impl Into<String>) -> Self {
52 Self::Disabled {
53 text: text.into(),
54 href: href.into(),
55 }
56 }
57
58 #[must_use]
60 pub const fn divider() -> Self {
61 Self::Divider
62 }
63
64 #[must_use]
66 pub fn header(text: impl Into<String>) -> Self {
67 Self::Header(text.into())
68 }
69
70 #[must_use]
72 pub fn text(text: impl Into<String>) -> Self {
73 Self::Text(text.into())
74 }
75}
76
77#[must_use]
96pub fn dropdown(color: Color, label: &str, items: &[DropdownItem]) -> Element<Div> {
97 let btn_class = format!("btn btn-{} dropdown-toggle", color.as_str());
98
99 Element::<Div>::new()
100 .class("dropdown")
101 .child::<Button, _>(|b| {
102 b.class(&btn_class)
103 .attr("type", "button")
104 .attr("data-bs-toggle", "dropdown")
105 .attr("aria-expanded", "false")
106 .text(label)
107 })
108 .child::<Ul, _>(|ul| dropdown_menu(ul, items))
109}
110
111#[must_use]
113pub fn dropdown_split(
114 color: Color,
115 label: &str,
116 href: &str,
117 items: &[DropdownItem],
118) -> Element<Div> {
119 let btn_class = format!("btn btn-{}", color.as_str());
120 let toggle_class = format!(
121 "btn btn-{} dropdown-toggle dropdown-toggle-split",
122 color.as_str()
123 );
124
125 Element::<Div>::new()
126 .class("btn-group")
127 .child::<A, _>(|a| a.class(&btn_class).attr("href", href).text(label))
128 .child::<Button, _>(|b| {
129 b.class(&toggle_class)
130 .attr("type", "button")
131 .attr("data-bs-toggle", "dropdown")
132 .attr("aria-expanded", "false")
133 .child::<Span, _>(|s| s.class("visually-hidden").text("Toggle Dropdown"))
134 })
135 .child::<Ul, _>(|ul| dropdown_menu(ul, items))
136}
137
138#[must_use]
140pub fn dropup(color: Color, label: &str, items: &[DropdownItem]) -> Element<Div> {
141 let btn_class = format!("btn btn-{} dropdown-toggle", color.as_str());
142
143 Element::<Div>::new()
144 .class("btn-group dropup")
145 .child::<Button, _>(|b| {
146 b.class(&btn_class)
147 .attr("type", "button")
148 .attr("data-bs-toggle", "dropdown")
149 .attr("aria-expanded", "false")
150 .text(label)
151 })
152 .child::<Ul, _>(|ul| dropdown_menu(ul, items))
153}
154
155#[must_use]
157pub fn dropstart(color: Color, label: &str, items: &[DropdownItem]) -> Element<Div> {
158 let btn_class = format!("btn btn-{} dropdown-toggle", color.as_str());
159
160 Element::<Div>::new()
161 .class("btn-group dropstart")
162 .child::<Button, _>(|b| {
163 b.class(&btn_class)
164 .attr("type", "button")
165 .attr("data-bs-toggle", "dropdown")
166 .attr("aria-expanded", "false")
167 .text(label)
168 })
169 .child::<Ul, _>(|ul| dropdown_menu(ul, items))
170}
171
172#[must_use]
174pub fn dropend(color: Color, label: &str, items: &[DropdownItem]) -> Element<Div> {
175 let btn_class = format!("btn btn-{} dropdown-toggle", color.as_str());
176
177 Element::<Div>::new()
178 .class("btn-group dropend")
179 .child::<Button, _>(|b| {
180 b.class(&btn_class)
181 .attr("type", "button")
182 .attr("data-bs-toggle", "dropdown")
183 .attr("aria-expanded", "false")
184 .text(label)
185 })
186 .child::<Ul, _>(|ul| dropdown_menu(ul, items))
187}
188
189#[must_use]
191pub fn dropdown_dark(color: Color, label: &str, items: &[DropdownItem]) -> Element<Div> {
192 let btn_class = format!("btn btn-{} dropdown-toggle", color.as_str());
193
194 Element::<Div>::new()
195 .class("dropdown")
196 .child::<Button, _>(|b| {
197 b.class(&btn_class)
198 .attr("type", "button")
199 .attr("data-bs-toggle", "dropdown")
200 .attr("aria-expanded", "false")
201 .text(label)
202 })
203 .child::<Ul, _>(|ul| dropdown_menu_dark(ul, items))
204}
205
206fn dropdown_menu(ul: Element<Ul>, items: &[DropdownItem]) -> Element<Ul> {
208 items
209 .iter()
210 .fold(ul.class("dropdown-menu"), |ul, item| match item {
211 DropdownItem::Link { text, href } => ul.child::<Li, _>(|li| {
212 li.child::<A, _>(|a| a.class("dropdown-item").attr("href", href).text(text))
213 }),
214 DropdownItem::Active { text, href } => ul.child::<Li, _>(|li| {
215 li.child::<A, _>(|a| {
216 a.class("dropdown-item active")
217 .attr("href", href)
218 .attr("aria-current", "true")
219 .text(text)
220 })
221 }),
222 DropdownItem::Disabled { text, href } => ul.child::<Li, _>(|li| {
223 li.child::<A, _>(|a| {
224 a.class("dropdown-item disabled")
225 .attr("href", href)
226 .attr("aria-disabled", "true")
227 .text(text)
228 })
229 }),
230 DropdownItem::Divider => {
231 ul.child::<Li, _>(|li| li.child::<Hr, _>(|hr| hr.class("dropdown-divider")))
232 }
233 DropdownItem::Header(text) => {
234 ul.child::<Li, _>(|li| li.child::<H6, _>(|h| h.class("dropdown-header").text(text)))
235 }
236 DropdownItem::Text(text) => ul.child::<Li, _>(|li| {
237 li.child::<Span, _>(|s| s.class("dropdown-item-text").text(text))
238 }),
239 })
240}
241
242use ironhtml_elements::H6;
243
244fn dropdown_menu_dark(ul: Element<Ul>, items: &[DropdownItem]) -> Element<Ul> {
246 items.iter().fold(
247 ul.class("dropdown-menu dropdown-menu-dark"),
248 |ul, item| match item {
249 DropdownItem::Link { text, href } => ul.child::<Li, _>(|li| {
250 li.child::<A, _>(|a| a.class("dropdown-item").attr("href", href).text(text))
251 }),
252 DropdownItem::Active { text, href } => ul.child::<Li, _>(|li| {
253 li.child::<A, _>(|a| {
254 a.class("dropdown-item active")
255 .attr("href", href)
256 .attr("aria-current", "true")
257 .text(text)
258 })
259 }),
260 DropdownItem::Disabled { text, href } => ul.child::<Li, _>(|li| {
261 li.child::<A, _>(|a| {
262 a.class("dropdown-item disabled")
263 .attr("href", href)
264 .attr("aria-disabled", "true")
265 .text(text)
266 })
267 }),
268 DropdownItem::Divider => {
269 ul.child::<Li, _>(|li| li.child::<Hr, _>(|hr| hr.class("dropdown-divider")))
270 }
271 DropdownItem::Header(text) => {
272 ul.child::<Li, _>(|li| li.child::<H6, _>(|h| h.class("dropdown-header").text(text)))
273 }
274 DropdownItem::Text(text) => ul.child::<Li, _>(|li| {
275 li.child::<Span, _>(|s| s.class("dropdown-item-text").text(text))
276 }),
277 },
278 )
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use alloc::vec;
285
286 #[test]
287 fn test_dropdown() {
288 let items = vec![
289 DropdownItem::link("Action", "#"),
290 DropdownItem::divider(),
291 DropdownItem::link("Another", "#"),
292 ];
293 let dd = dropdown(Color::Primary, "Menu", &items);
294 let html = dd.render();
295 assert!(html.contains("dropdown"));
296 assert!(html.contains("dropdown-toggle"));
297 assert!(html.contains("dropdown-menu"));
298 assert!(html.contains("dropdown-item"));
299 }
300
301 #[test]
302 fn test_dropdown_split() {
303 let items = vec![DropdownItem::link("Action", "#")];
304 let dd = dropdown_split(Color::Success, "Action", "#", &items);
305 let html = dd.render();
306 assert!(html.contains("btn-group"));
307 assert!(html.contains("dropdown-toggle-split"));
308 }
309
310 #[test]
311 fn test_dropup() {
312 let items = vec![DropdownItem::link("Action", "#")];
313 let dd = dropup(Color::Primary, "Dropup", &items);
314 assert!(dd.render().contains("dropup"));
315 }
316
317 #[test]
318 fn test_dropdown_with_header() {
319 let items = vec![
320 DropdownItem::header("Dropdown header"),
321 DropdownItem::link("Action", "#"),
322 ];
323 let dd = dropdown(Color::Secondary, "Menu", &items);
324 assert!(dd.render().contains("dropdown-header"));
325 }
326}