accordion_rs/leptos.rs
1use crate::common::{Align, Size};
2use leptos::prelude::*;
3
4/// Accordion Component
5///
6/// A Leptos component for displaying an accordion-style UI element that can be expanded or collapsed.
7/// This `Accordion` component supports customizing its appearance, animations, and behavior during
8/// expansion or collapse. The component can hold content within a collapsible section, which can be
9/// expanded or collapsed by the user. It also supports various customization options, such as custom
10/// styles, classes, and accessibility features like ARIA attributes.
11///
12/// # Properties
13///
14/// - **expand**: A tuple signal containing a `ReadSignal<bool>` and a `WriteSignal<bool>` for tracking and updating the expansion state of the accordion.
15/// - **expanded**: A view content to display when the accordion is expanded (`Box<dyn Fn() -> AnyView>`).
16/// - **collapsed**: A view content to display when the accordion is collapsed (`Box<dyn Fn() -> AnyView>`).
17/// - **children**: Child elements inside the accordion (`Children`).
18/// - **size**: Defines the size of the accordion (`Size`). Default: `Size::XXLarge`.
19/// - **aria_controls**: The ARIA controls attribute for accessibility (`&'static str`). Default: `""`.
20/// - **style**: Inline styles applied to the accordion container (`&'static str`). Default: `""`.
21/// - **expanded_style**: Inline styles applied when the accordion is expanded (`&'static str`). Default: `""`.
22/// - **collapsed_style**: Inline styles applied when the accordion is collapsed (`&'static str`). Default: `""`.
23/// - **content_style**: Inline styles applied to the accordion's content container (`&'static str`). Default: `""`.
24/// - **class**: CSS class for the accordion container (`&'static str`). Default: `""`.
25/// - **expanded_class**: CSS class applied when the accordion is expanded (`&'static str`). Default: `""`.
26/// - **collapsed_class**: CSS class applied when the accordion is collapsed (`&'static str`). Default: `""`.
27/// - **content_class**: CSS class applied to the content container (`&'static str`). Default: `""`.
28/// - **aria_enabled**: Whether ARIA attributes should be included for accessibility (`bool`). Default: `true`.
29/// - **duration**: Duration of the expand/collapse transition in milliseconds (`u64`). Default: `600`.
30/// - **will_open**: Callback triggered before the accordion starts expanding (`Callback<()>`). Default: no-op.
31/// - **did_open**: Callback triggered after the accordion finishes expanding (`Callback<()>`). Default: no-op.
32/// - **will_close**: Callback triggered before the accordion starts collapsing (`Callback<()>`). Default: no-op.
33/// - **did_close**: Callback triggered after the accordion finishes collapsing (`Callback<()>`). Default: no-op.
34///
35/// # Features
36/// - Customizable expanded and collapsed content.
37/// - Animation duration for smooth transitions between states.
38/// - ARIA accessibility features for improved screen reader support.
39/// - Callbacks for tracking expansion and collapse events.
40///
41/// # Examples
42///
43/// ## Basic Usage
44/// ```rust
45/// use leptos::prelude::*;
46/// use accordion_rs::leptos::{Accordion, List, Item};
47/// use accordion_rs::{Align};
48///
49/// #[component]
50/// pub fn App() -> impl IntoView {
51/// let expanded = signal(false);
52///
53/// view! {
54/// <Accordion
55/// expand={expanded}
56/// expanded={Box::new(|| view! { <p>"This is expanded content."</p> }.into_any())}
57/// collapsed={Box::new(|| view! { <p>"This is collapsed content."</p> }.into_any())}
58/// >
59/// <List>
60/// <Item align={Align::Left}>{ "Item 1 - Left" }</Item>
61/// <Item align={Align::Right}>{ "Item 2 - Right" }</Item>
62/// </List>
63/// </Accordion>
64/// }
65/// }
66/// ```
67///
68/// ## Accordion with Custom Styles
69/// ```rust
70/// use leptos::prelude::*;
71/// use accordion_rs::leptos::{Accordion, List, Item};
72///
73/// #[component]
74/// pub fn App() -> impl IntoView {
75/// let expanded = signal(false);
76///
77/// view! {
78/// <Accordion
79/// expand={expanded}
80/// expanded={Box::new(|| view! { <p>"Expanded content."</p> }.into_any())}
81/// collapsed={Box::new(|| view! { <p>"Collapsed content."</p> }.into_any())}
82/// style="background-color: lightblue; padding: 10px;"
83/// expanded_style="background-color: lightgreen;"
84/// collapsed_style="background-color: lightcoral;"
85/// >
86/// <List>
87/// <Item>{ "Item 1 - Left" }</Item>
88/// <Item>{ "Item 2 - Right" }</Item>
89/// </List>
90/// </Accordion>
91/// }
92/// }
93/// ```
94///
95/// ## Accordion with Callbacks
96/// ```rust
97/// use leptos::prelude::*;
98/// use leptos::logging::log;
99/// use accordion_rs::leptos::{Accordion, List, Item};
100///
101/// #[component]
102/// pub fn App() -> impl IntoView {
103/// let expanded = signal(false);
104///
105/// let will_open = move || log!("Accordion is about to open.");
106/// let did_open = move || log!("Accordion has opened.");
107/// let will_close = move || log!("Accordion is about to close.");
108/// let did_close = move || log!("Accordion has closed.");
109///
110/// view! {
111/// <Accordion
112/// expand={expanded}
113/// expanded={Box::new(|| view! { <p>"Expanded content."</p> }.into_any())}
114/// collapsed={Box::new(|| view! { <p>"Collapsed content."</p> }.into_any())}
115/// will_open={Callback::from(will_open)}
116/// did_open={Callback::from(did_open)}
117/// will_close={Callback::from(will_close)}
118/// did_close={Callback::from(did_close)}
119/// >
120/// <List>
121/// <Item>{ "Item 1 - Left" }</Item>
122/// <Item>{ "Item 2 - Right" }</Item>
123/// </List>
124/// </Accordion>
125/// }
126/// }
127/// ```
128///
129/// # Behavior
130/// - The accordion toggles between expanded and collapsed states based on the `expand` signal.
131/// - Transitions between states are smooth, with customizable duration.
132/// - Callbacks allow you to hook into the expand/collapse lifecycle events.
133/// - ARIA attributes can be toggled for better accessibility when `aria_enabled` is `true`.
134///
135/// # Notes
136/// - The `size` property determines the overall size of the accordion (e.g., `Size::Small`, `Size::Medium`).
137/// - Use inline styles or CSS classes for detailed customization of the accordion's appearance.
138/// - Default callbacks (`will_open`, `did_open`, `will_close`, `did_close`) are no-ops but can be customized as needed.
139#[component]
140pub fn Accordion(
141 /// Signal to track if the accordion is expanded.
142 ///
143 /// This is a tuple containing a `ReadSignal` to observe the expanded state
144 /// and a `WriteSignal` to update it. Use this to programmatically control or
145 /// react to the accordion's expansion.
146 expand: (ReadSignal<bool>, WriteSignal<bool>),
147
148 /// Content to display when the accordion is expanded.
149 ///
150 /// This is a function returning an `AnyView` that will be rendered inside the accordion
151 /// when it is in an expanded state.
152 expanded: Box<dyn Fn() -> AnyView + Send + Sync>,
153
154 /// Content to display when the accordion is collapsed.
155 ///
156 /// This is a function returning an `AnyView` that will be rendered inside the accordion
157 /// when it is in a collapsed state.
158 collapsed: Box<dyn Fn() -> AnyView + Send + Sync>,
159
160 /// Child elements inside the accordion.
161 ///
162 /// These are additional elements that are rendered as part of the accordion's body.
163 children: ChildrenFn,
164
165 /// Size of the accordion.
166 ///
167 /// This defines the overall size of the accordion component. Acceptable values
168 /// are defined by the `Size` enum, and the default value is `Size::XXLarge`.
169 #[prop(default = Size::XXLarge)]
170 size: Size,
171
172 /// ARIA controls attribute.
173 ///
174 /// Sets the value for the `aria-controls` attribute, which is used for accessibility
175 /// purposes to associate the accordion header with its content. Defaults to an empty string.
176 #[prop(default = "")]
177 aria_controls: &'static str,
178
179 /// Inline style for the accordion.
180 ///
181 /// Applies custom inline styles to the accordion container. Defaults to an empty string.
182 #[prop(default = "")]
183 style: &'static str,
184
185 /// Style when the accordion is expanded.
186 ///
187 /// Defines additional inline styles applied to the accordion when it is expanded.
188 /// Defaults to an empty string.
189 #[prop(default = "")]
190 expanded_style: &'static str,
191
192 /// Style when the accordion is collapsed.
193 ///
194 /// Defines additional inline styles applied to the accordion when it is collapsed.
195 /// Defaults to an empty string.
196 #[prop(default = "")]
197 collapsed_style: &'static str,
198
199 /// Style for the accordion's content container.
200 ///
201 /// Sets inline styles for the container that wraps the accordion's content.
202 /// Defaults to an empty string.
203 #[prop(default = "")]
204 content_style: &'static str,
205
206 /// CSS class for the accordion.
207 ///
208 /// Adds a CSS class to the accordion container for styling purposes. Defaults to an empty string.
209 #[prop(default = "")]
210 class: &'static str,
211
212 /// CSS class when the accordion is expanded.
213 ///
214 /// Adds a CSS class to the accordion container when it is in an expanded state.
215 /// Defaults to an empty string.
216 #[prop(default = "")]
217 expanded_class: &'static str,
218
219 /// CSS class when the accordion is collapsed.
220 ///
221 /// Adds a CSS class to the accordion container when it is in a collapsed state.
222 /// Defaults to an empty string.
223 #[prop(default = "")]
224 collapsed_class: &'static str,
225
226 /// CSS class for the content container.
227 ///
228 /// Adds a CSS class to the container that wraps the accordion's content. Defaults to an empty string.
229 #[prop(default = "")]
230 content_class: &'static str,
231
232 /// Whether to include ARIA attributes.
233 ///
234 /// If `true`, ARIA attributes are included to improve accessibility. Defaults to `true`.
235 #[prop(default = true)]
236 aria_enabled: bool,
237
238 /// Duration of the expand/collapse transition in milliseconds.
239 ///
240 /// Sets the time it takes for the accordion to transition between expanded and collapsed states.
241 /// Defaults to `600` milliseconds.
242 #[prop(default = 600)]
243 duration: u64,
244
245 /// Callback for when the accordion starts opening.
246 ///
247 /// This callback is invoked at the start of the accordion's expand transition.
248 /// Defaults to no-op.
249 #[prop(default = Callback::from(|| {}))]
250 will_open: Callback<()>,
251
252 /// Callback for when the accordion finishes opening.
253 ///
254 /// This callback is invoked after the accordion has fully expanded.
255 /// Defaults to no-op.
256 #[prop(default = Callback::from(|| {}))]
257 did_open: Callback<()>,
258
259 /// Callback for when the accordion starts closing.
260 ///
261 /// This callback is invoked at the start of the accordion's collapse transition.
262 /// Defaults to no-op.
263 #[prop(default = Callback::from(|| {}))]
264 will_close: Callback<()>,
265
266 /// Callback for when the accordion finishes closing.
267 ///
268 /// This callback is invoked after the accordion has fully collapsed.
269 /// Defaults to no-op.
270 #[prop(default = Callback::from(|| {}))]
271 did_close: Callback<()>,
272) -> impl IntoView {
273 let toggle_expansion = move || {
274 if expand.0.get() {
275 will_close.run(());
276 expand.1.set(false);
277 did_close.run(());
278 } else {
279 will_open.run(());
280 expand.1.set(true);
281 did_open.run(());
282 }
283 };
284
285 view! {
286 <div
287 style=format!("{} {}", size.to_style(), style)
288 class=class
289 >
290 <div
291 aria-expanded={move || if aria_enabled { Some(expand.0.get().to_string()) } else { None }}
292 aria-controls=aria_controls
293 on:click=move |_| toggle_expansion()
294 class=move || if expand.0.get() { expanded_class } else { collapsed_class }
295 style=move || format!(
296 "cursor: pointer; transition: all {}ms; {}",
297 duration,
298 if expand.0.get() { expanded_style } else { collapsed_style }
299 )
300 >
301 {move || {
302 if expand.0.get() {
303 expanded()
304 } else {
305 collapsed()
306 }
307 }}
308 </div>
309 <Show when=move || expand.0.get() clone:children>
310 <div
311 id=aria_controls
312 class=content_class
313 style=format!(
314 "overflow: hidden; transition: all {}ms; {}",
315 duration,
316 content_style
317 )
318 >
319 {children()}
320 </div>
321 </Show>
322 </div>
323 }
324}
325
326#[component]
327pub fn Item(
328 /// Child content of the Item
329 children: Children,
330
331 /// Additional styles for the Item
332 #[prop(default = "")]
333 style: &'static str,
334
335 /// CSS class for the Item
336 #[prop(default = "")]
337 class: &'static str,
338
339 /// Alignment for the content
340 #[prop(default = Align::Left)]
341 align: Align,
342
343 /// Title of the Item
344 #[prop(default = "")]
345 title: &'static str,
346
347 /// Optional icon for the Item
348 #[prop(default = "")]
349 icon: &'static str,
350) -> impl IntoView {
351 view! {
352 <li
353 class=class
354 style=format!("{} {}", align.to_style(), style)
355 >
356 {move || {
357 if !icon.is_empty() {
358 Some(view! { <span class="mr-2">{icon}</span> })
359 } else {
360 None
361 }
362 }}
363 {move || {
364 if !title.is_empty() {
365 Some(view! { <strong>{title}</strong> })
366 } else {
367 None
368 }
369 }}
370 {children()}
371 </li>
372 }
373}
374
375#[component]
376pub fn Button(
377 /// Content for the Button
378 children: Children,
379
380 /// Styles for the Button
381 #[prop(default = "")]
382 style: &'static str,
383
384 /// CSS class for the Button
385 #[prop(default = "")]
386 class: &'static str,
387) -> impl IntoView {
388 view! {
389 <button class=class style=style>
390 {children()}
391 </button>
392 }
393}
394
395#[component]
396pub fn List(
397 /// Child items for the List
398 children: Children,
399
400 /// Styles for the List
401 #[prop(default = "")]
402 style: &'static str,
403
404 /// CSS class for the List
405 #[prop(default = "")]
406 class: &'static str,
407) -> impl IntoView {
408 view! {
409 <ul class=class style=style>
410 {children()}
411 </ul>
412 }
413}