Skip to main content

dioxus_bootstrap_css/
accordion.rs

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