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