ironhtml_bootstrap/
accordion.rs

1//! Bootstrap accordion components.
2//!
3//! Provides type-safe Bootstrap accordions matching the
4//! [Bootstrap accordion documentation](https://getbootstrap.com/docs/5.3/components/accordion/).
5
6use ironhtml::typed::Element;
7use ironhtml_elements::{Button, Div, H2};
8
9extern crate alloc;
10use alloc::format;
11use alloc::string::String;
12
13/// An accordion item with header and content.
14pub struct AccordionItem {
15    pub id: String,
16    pub header: String,
17    pub content: String,
18    pub expanded: bool,
19}
20
21/// Create a Bootstrap accordion.
22///
23/// ## Example
24///
25/// ```rust
26/// use ironhtml_bootstrap::accordion::{accordion, AccordionItem};
27///
28/// let items = vec![
29///     AccordionItem {
30///         id: "one".into(),
31///         header: "Accordion Item #1".into(),
32///         content: "This is the first item's content.".into(),
33///         expanded: true,
34///     },
35///     AccordionItem {
36///         id: "two".into(),
37///         header: "Accordion Item #2".into(),
38///         content: "This is the second item's content.".into(),
39///         expanded: false,
40///     },
41/// ];
42///
43/// let acc = accordion("accordionExample", &items);
44/// assert!(acc.render().contains("accordion"));
45/// ```
46#[must_use]
47pub fn accordion(id: &str, items: &[AccordionItem]) -> Element<Div> {
48    let mut acc = Element::<Div>::new().class("accordion").id(id);
49
50    for item in items {
51        acc = acc.child::<Div, _>(|_| accordion_item(id, item));
52    }
53
54    acc
55}
56
57/// Create a flush accordion (no borders, square corners).
58#[must_use]
59pub fn accordion_flush(id: &str, items: &[AccordionItem]) -> Element<Div> {
60    let mut acc = Element::<Div>::new()
61        .class("accordion accordion-flush")
62        .id(id);
63
64    for item in items {
65        acc = acc.child::<Div, _>(|_| accordion_item(id, item));
66    }
67
68    acc
69}
70
71/// Create a single accordion item.
72fn accordion_item(parent_id: &str, item: &AccordionItem) -> Element<Div> {
73    let collapse_id = format!("collapse{}", item.id);
74    let heading_id = format!("heading{}", item.id);
75    let target = format!("#{collapse_id}");
76    let parent = format!("#{parent_id}");
77
78    let button_class = if item.expanded {
79        "accordion-button"
80    } else {
81        "accordion-button collapsed"
82    };
83
84    let collapse_class = if item.expanded {
85        "accordion-collapse collapse show"
86    } else {
87        "accordion-collapse collapse"
88    };
89
90    Element::<Div>::new()
91        .class("accordion-item")
92        .child::<H2, _>(|h| {
93            h.class("accordion-header")
94                .id(&heading_id)
95                .child::<Button, _>(|btn| {
96                    btn.class(button_class)
97                        .attr("type", "button")
98                        .attr("data-bs-toggle", "collapse")
99                        .attr("data-bs-target", &target)
100                        .attr(
101                            "aria-expanded",
102                            if item.expanded { "true" } else { "false" },
103                        )
104                        .attr("aria-controls", &collapse_id)
105                        .text(&item.header)
106                })
107        })
108        .child::<Div, _>(|collapse| {
109            collapse
110                .id(&collapse_id)
111                .class(collapse_class)
112                .attr("aria-labelledby", &heading_id)
113                .attr("data-bs-parent", &parent)
114                .child::<Div, _>(|body| body.class("accordion-body").text(&item.content))
115        })
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use alloc::vec;
122
123    #[test]
124    fn test_accordion() {
125        let items = vec![
126            AccordionItem {
127                id: "one".into(),
128                header: "First".into(),
129                content: "Content 1".into(),
130                expanded: true,
131            },
132            AccordionItem {
133                id: "two".into(),
134                header: "Second".into(),
135                content: "Content 2".into(),
136                expanded: false,
137            },
138        ];
139
140        let acc = accordion("test", &items);
141        let html = acc.render();
142        assert!(html.contains("accordion"));
143        assert!(html.contains("accordion-item"));
144        assert!(html.contains("accordion-button"));
145        assert!(html.contains("collapse show"));
146        assert!(html.contains("collapsed"));
147    }
148}