Skip to main content

dioxus_bootstrap_css/
accordion.rs

1use dioxus::prelude::*;
2
3/// Bootstrap Accordion component — signal-driven, no JavaScript.
4///
5/// # Bootstrap HTML → Dioxus
6///
7/// ```html
8/// <!-- Bootstrap HTML (requires JavaScript) -->
9/// <div class="accordion">
10///   <div class="accordion-item">
11///     <h2 class="accordion-header">
12///       <button class="accordion-button" data-bs-toggle="collapse">Section 1</button>
13///     </h2>
14///     <div class="accordion-collapse collapse show">
15///       <div class="accordion-body">Content 1</div>
16///     </div>
17///   </div>
18/// </div>
19/// ```
20///
21/// ```rust,no_run
22/// # use dioxus::prelude::*;
23/// # use dioxus_bootstrap_css::prelude::*;
24/// # fn _doctest() -> Element {
25/// // Dioxus equivalent
26/// let open = use_signal(|| Some(0usize));
27/// rsx! {
28///     Accordion { open: open, flush: true,
29///         AccordionItem { index: 0, title: "Section 1", open: open,
30///             p { "Content for section 1" }
31///         }
32///         AccordionItem { index: 1, title: "Section 2", open: open,
33///             p { "Content for section 2" }
34///         }
35///     }
36/// }
37/// # }
38/// ```
39///
40/// # Props
41///
42/// - `open` — `Signal<Option<usize>>` controlling which item is open (None = all closed)
43/// - `flush` — remove borders and rounded corners
44#[derive(Clone, PartialEq, Props)]
45pub struct AccordionProps {
46    /// Signal controlling which item is open (None = all closed).
47    /// For "always open" mode, use AccordionAlwaysOpen instead.
48    pub open: Signal<Option<usize>>,
49    /// Remove borders and rounded corners.
50    #[props(default)]
51    pub flush: bool,
52    /// Additional CSS classes.
53    #[props(default)]
54    pub class: String,
55    /// Any additional HTML attributes.
56    #[props(extends = GlobalAttributes)]
57    attributes: Vec<Attribute>,
58    /// Child elements (AccordionItem components).
59    pub children: Element,
60}
61
62#[component]
63pub fn Accordion(props: AccordionProps) -> Element {
64    let flush = if props.flush { " accordion-flush" } else { "" };
65    let full_class = if props.class.is_empty() {
66        format!("accordion{flush}")
67    } else {
68        format!("accordion{flush} {}", props.class)
69    };
70
71    rsx! {
72        div { class: "{full_class}", ..props.attributes, {props.children} }
73    }
74}
75
76/// A single item within an Accordion.
77#[derive(Clone, PartialEq, Props)]
78pub struct AccordionItemProps {
79    /// Item index (must match position in accordion).
80    pub index: usize,
81    /// Header/title text.
82    pub title: String,
83    /// Signal controlling which item is open (shared with parent).
84    pub open: Signal<Option<usize>>,
85    /// Additional CSS classes for the accordion item.
86    #[props(default)]
87    pub class: String,
88    /// Any additional HTML attributes.
89    #[props(extends = GlobalAttributes)]
90    attributes: Vec<Attribute>,
91    /// Content (shown when expanded).
92    pub children: Element,
93}
94
95#[component]
96pub fn AccordionItem(props: AccordionItemProps) -> Element {
97    let is_open = *props.open.read() == Some(props.index);
98    let mut open_signal = props.open;
99    let index = props.index;
100
101    let button_class = if is_open {
102        "accordion-button"
103    } else {
104        "accordion-button collapsed"
105    };
106
107    let body_class = if is_open {
108        "accordion-collapse collapse show"
109    } else {
110        "accordion-collapse collapse"
111    };
112
113    let full_class = if props.class.is_empty() {
114        "accordion-item".to_string()
115    } else {
116        format!("accordion-item {}", props.class)
117    };
118
119    rsx! {
120        div { class: "{full_class}",
121            ..props.attributes,
122            h2 { class: "accordion-header",
123                button {
124                    class: "{button_class}",
125                    r#type: "button",
126                    "aria-expanded": if is_open { "true" } else { "false" },
127                    onclick: move |_| {
128                        if is_open {
129                            open_signal.set(None);
130                        } else {
131                            open_signal.set(Some(index));
132                        }
133                    },
134                    "{props.title}"
135                }
136            }
137            div { class: "{body_class}",
138                div { class: "accordion-body",
139                    {props.children}
140                }
141            }
142        }
143    }
144}