dioxus_tw_components/components/molecules/accordion/
props.rs1use crate::{attributes::*, components::atoms::icon::*};
2use dioxus::prelude::*;
3use dioxus_core::AttributeValue;
4use dioxus_tw_components_macro::UiComp;
5
6struct AccordionState {
7 multi_open: bool,
8 active_items: Vec<String>,
9}
10
11impl AccordionState {
12 fn new(multi_open: bool) -> Self {
13 Self {
14 multi_open,
15 active_items: Vec::with_capacity(1),
16 }
17 }
18
19 fn add_id(&mut self, id: String) {
20 self.active_items.push(id);
21 }
22
23 fn remove_id(&mut self, id: String) {
24 self.active_items.retain(|x| x != &id);
25 }
26
27 fn set_id(&mut self, id: String) {
28 self.active_items.clear();
29 self.active_items.push(id);
30 }
31
32 fn is_active(&self, id: &str) -> bool {
33 self.active_items.contains(&id.to_string())
34 }
35
36 fn is_active_to_attr_value(&self, id: String) -> AttributeValue {
37 match self.active_items.contains(&id) {
38 true => AttributeValue::Text("active".to_string()),
39 false => AttributeValue::Text("inactive".to_string()),
40 }
41 }
42}
43
44#[derive(Clone, PartialEq, Props, UiComp)]
45pub struct AccordionProps {
46 #[props(extends = div, extends = GlobalAttributes)]
47 attributes: Vec<Attribute>,
48
49 #[props(default = false)]
51 multi_open: bool,
52
53 children: Element,
54}
55
56impl std::default::Default for AccordionProps {
57 fn default() -> Self {
58 Self {
59 attributes: Vec::<Attribute>::default(),
60 multi_open: false,
61 children: rsx! {},
62 }
63 }
64}
65
66#[component]
81pub fn Accordion(mut props: AccordionProps) -> Element {
82 use_context_provider(|| Signal::new(AccordionState::new(props.multi_open)));
83
84 props.update_class_attribute();
85
86 rsx! {
87 div { ..props.attributes,{props.children} }
88 }
89}
90
91#[derive(Clone, PartialEq, Props, UiComp)]
92pub struct AccordionItemProps {
93 #[props(extends = div, extends = GlobalAttributes)]
94 attributes: Vec<Attribute>,
95
96 children: Element,
97}
98
99impl std::default::Default for AccordionItemProps {
100 fn default() -> Self {
101 Self {
102 attributes: Vec::<Attribute>::default(),
103 children: rsx! {},
104 }
105 }
106}
107
108#[component]
110pub fn AccordionItem(mut props: AccordionItemProps) -> Element {
111 props.update_class_attribute();
112
113 rsx! {
114 div { ..props.attributes,{props.children} }
115 }
116}
117
118#[derive(Clone, PartialEq, Props, UiComp)]
119pub struct AccordionTriggerProps {
120 #[props(extends = button, extends = GlobalAttributes)]
121 attributes: Vec<Attribute>,
122
123 #[props(optional)]
124 id: ReadOnlySignal<String>,
125
126 #[props(optional, into)]
128 is_open: ReadOnlySignal<bool>,
129
130 #[props(optional, default)]
131 onclick: EventHandler<MouseEvent>,
132
133 #[props(optional, default = default_trigger_decoration())]
135 trigger_decoration: Element,
136
137 children: Element,
138}
139
140impl std::default::Default for AccordionTriggerProps {
141 fn default() -> Self {
142 Self {
143 attributes: Vec::<Attribute>::default(),
144 id: ReadOnlySignal::<String>::default(),
145 is_open: ReadOnlySignal::<bool>::default(),
146 onclick: EventHandler::<MouseEvent>::default(),
147 trigger_decoration: default_trigger_decoration(),
148 children: rsx! {},
149 }
150 }
151}
152
153#[component]
155pub fn AccordionTrigger(mut props: AccordionTriggerProps) -> Element {
156 props.update_class_attribute();
157
158 let mut state = use_context::<Signal<AccordionState>>();
159
160 use_effect(move || {
161 if *props.is_open.read() {
162 if !state.peek().multi_open {
163 state.write().set_id(props.id.read().clone());
164 } else {
165 state.write().add_id(props.id.read().clone());
166 }
167 } else if state.peek().is_active(&props.id.read()) {
168 state.write().remove_id(props.id.read().clone());
169 }
170 });
171
172 let button_closure = move |event: Event<MouseData>| {
173 if state.read().is_active(&props.id.read()) {
175 state.write().remove_id(props.id.read().clone());
176 } else {
177 if !state.read().multi_open {
181 state.write().set_id(props.id.read().clone());
182 } else {
183 state.write().add_id(props.id.read().clone());
184 }
185 }
186 props.onclick.call(event)
187 };
188
189 rsx! {
190 button {
191 "data-state": state.read().is_active_to_attr_value(props.id.read().to_string()),
192 onclick: button_closure,
193 ..props.attributes,
194 {props.children}
195 {props.trigger_decoration}
196 }
197 }
198}
199
200fn default_trigger_decoration() -> Element {
201 rsx! {
202 Icon {
203 class: "transition-transform transform duration-300 group-data-[state=active]:-rotate-180",
204 icon: Icons::ExpandMore,
205 }
206 }
207}
208
209#[derive(Clone, PartialEq, Props, UiComp)]
210pub struct AccordionContentProps {
211 #[props(extends = div, extends = GlobalAttributes)]
212 attributes: Vec<Attribute>,
213
214 #[props(optional)]
215 id: ReadOnlySignal<String>,
216
217 #[props(optional)]
218 height: ReadOnlySignal<String>,
219
220 #[props(default)]
221 pub animation: ReadOnlySignal<Animation>,
222
223 children: Element,
224}
225
226impl std::default::Default for AccordionContentProps {
227 fn default() -> Self {
228 Self {
229 attributes: Vec::<Attribute>::default(),
230 id: ReadOnlySignal::<String>::default(),
231 height: ReadOnlySignal::<String>::default(),
232 animation: ReadOnlySignal::<Animation>::default(),
233 children: rsx! {},
234 }
235 }
236}
237
238#[component]
240pub fn AccordionContent(mut props: AccordionContentProps) -> Element {
241 let mut elem_height = use_signal(|| "".to_string());
243
244 props.update_class_attribute();
245
246 let state = use_context::<Signal<AccordionState>>();
247
248 let final_height = match state.read().is_active(&props.id.read()) {
249 true => {
250 if props.height.read().is_empty() {
251 elem_height()
252 } else {
253 props.height.read().clone()
254 }
255 }
256 false => "0".to_string(),
257 };
258
259 rsx! {
260 div {
261 onmounted: move |element| async move {
262 if !props.height.read().is_empty() {
263 return;
264 }
265 if props.animation == Animation::None {
266 elem_height.set("auto".to_string());
267 return;
268 }
269 elem_height
270 .set(
271 match element.data().get_scroll_size().await {
272 Ok(size) => format!("{}px", size.height),
273 Err(e) => {
274 log::error!(
275 "AccordionContent: Failed to get element height(id probably not set): setting it to auto: {e:?}",
276 );
277 "auto".to_string()
278 }
279 },
280 );
281 },
282 "data-state": state.read().is_active_to_attr_value(props.id.read().to_string()),
283 id: props.id,
284 height: final_height,
285 ..props.attributes,
286 {props.children}
287 }
288 }
289}