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}