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 /// Child elements (AccordionItem components).
52 pub children: Element,
53}
54
55#[component]
56pub fn Accordion(props: AccordionProps) -> Element {
57 let flush = if props.flush { " accordion-flush" } else { "" };
58 let full_class = if props.class.is_empty() {
59 format!("accordion{flush}")
60 } else {
61 format!("accordion{flush} {}", props.class)
62 };
63
64 rsx! {
65 div { class: "{full_class}", {props.children} }
66 }
67}
68
69/// A single item within an Accordion.
70#[derive(Clone, PartialEq, Props)]
71pub struct AccordionItemProps {
72 /// Item index (must match position in accordion).
73 pub index: usize,
74 /// Header/title text.
75 pub title: String,
76 /// Signal controlling which item is open (shared with parent).
77 pub open: Signal<Option<usize>>,
78 /// Additional CSS classes for the accordion item.
79 #[props(default)]
80 pub class: String,
81 /// Content (shown when expanded).
82 pub children: Element,
83}
84
85#[component]
86pub fn AccordionItem(props: AccordionItemProps) -> Element {
87 let is_open = *props.open.read() == Some(props.index);
88 let mut open_signal = props.open;
89 let index = props.index;
90
91 let button_class = if is_open {
92 "accordion-button"
93 } else {
94 "accordion-button collapsed"
95 };
96
97 let body_class = if is_open {
98 "accordion-collapse collapse show"
99 } else {
100 "accordion-collapse collapse"
101 };
102
103 let full_class = if props.class.is_empty() {
104 "accordion-item".to_string()
105 } else {
106 format!("accordion-item {}", props.class)
107 };
108
109 rsx! {
110 div { class: "{full_class}",
111 h2 { class: "accordion-header",
112 button {
113 class: "{button_class}",
114 r#type: "button",
115 "aria-expanded": if is_open { "true" } else { "false" },
116 onclick: move |_| {
117 if is_open {
118 open_signal.set(None);
119 } else {
120 open_signal.set(Some(index));
121 }
122 },
123 "{props.title}"
124 }
125 }
126 div { class: "{body_class}",
127 div { class: "accordion-body",
128 {props.children}
129 }
130 }
131 }
132 }
133}