Skip to main content

freya_components/
accordion.rs

1use dioxus::prelude::*;
2use freya_core::platform::CursorIcon;
3use freya_elements::{
4    self as dioxus_elements,
5    events::MouseEvent,
6};
7use freya_hooks::{
8    use_animation,
9    use_applied_theme,
10    use_platform,
11    AccordionTheme,
12    AccordionThemeWith,
13    AnimNum,
14    Ease,
15    Function,
16};
17
18/// Indicates the current status of the accordion.
19#[derive(Debug, Default, PartialEq, Clone, Copy)]
20pub enum AccordionStatus {
21    /// Default state.
22    #[default]
23    Idle,
24    /// Mouse is hovering the accordion.
25    Hovering,
26}
27
28/// Properties for the [`Accordion`] component.
29#[derive(Props, Clone, PartialEq)]
30pub struct AccordionProps {
31    /// Theme override.
32    pub theme: Option<AccordionThemeWith>,
33    /// Inner children for the Accordion.
34    pub children: Element,
35    /// Summary element.
36    pub summary: Element,
37    /// Whether its open or not initially. Default to `false`.
38    #[props(default = false)]
39    pub initial_open: bool,
40}
41
42/// Show other elements under a collapsable box.
43///
44/// # Styling
45/// Inherits the [`AccordionTheme`](freya_hooks::AccordionTheme)
46#[allow(non_snake_case)]
47pub fn Accordion(props: AccordionProps) -> Element {
48    let theme = use_applied_theme!(&props.theme, accordion);
49    let mut open = use_signal(|| props.initial_open);
50    let animation = use_animation(move |_conf| {
51        AnimNum::new(0., 100.)
52            .time(300)
53            .function(Function::Expo)
54            .ease(Ease::Out)
55    });
56    let mut status = use_signal(AccordionStatus::default);
57    let platform = use_platform();
58
59    let animation_value = animation.get().read().read();
60    let AccordionTheme {
61        background,
62        color,
63        border_fill,
64    } = theme;
65
66    let onclick = move |_: MouseEvent| {
67        open.toggle();
68        if *open.read() {
69            animation.start();
70        } else {
71            animation.reverse();
72        }
73    };
74
75    use_drop(move || {
76        if *status.read() == AccordionStatus::Hovering {
77            platform.set_cursor(CursorIcon::default());
78        }
79    });
80
81    let onmouseenter = move |_| {
82        platform.set_cursor(CursorIcon::Pointer);
83        status.set(AccordionStatus::Hovering);
84    };
85
86    let onmouseleave = move |_| {
87        platform.set_cursor(CursorIcon::default());
88        status.set(AccordionStatus::default());
89    };
90
91    rsx!(
92        rect {
93            onmouseenter,
94            onmouseleave,
95            overflow: "clip",
96            color: "{color}",
97            padding: "10",
98            margin: "2 4",
99            corner_radius: "6",
100            width: "100%",
101            height: "auto",
102            background: "{background}",
103            onclick,
104            border: "1 inner {border_fill}",
105            {&props.summary}
106            rect {
107                overflow: "clip",
108                width: "100%",
109                visible_height: "{animation_value}%",
110                {&props.children}
111            }
112        }
113    )
114}
115
116/// Properties for the [`AccordionSummary`] component.
117#[derive(Props, Clone, PartialEq)]
118pub struct AccordionSummaryProps {
119    /// Inner children for the AccordionSummary.
120    children: Element,
121}
122
123/// Intended to use as summary for an [`Accordion`].
124#[allow(non_snake_case)]
125pub fn AccordionSummary(props: AccordionSummaryProps) -> Element {
126    rsx!({ props.children })
127}
128
129/// Properties for the [`AccordionBody`] component.
130#[derive(Props, Clone, PartialEq)]
131pub struct AccordionBodyProps {
132    /// Inner children for the AccordionBody.
133    children: Element,
134}
135
136/// Intended to wrap the body of an [`Accordion`].
137#[allow(non_snake_case)]
138pub fn AccordionBody(props: AccordionBodyProps) -> Element {
139    rsx!(rect {
140        width: "100%",
141        padding: "15 0 0 0",
142        {props.children}
143    })
144}
145
146#[cfg(test)]
147mod test {
148    use std::time::Duration;
149
150    use freya::prelude::*;
151    use freya_testing::prelude::*;
152    use tokio::time::sleep;
153
154    #[tokio::test]
155    pub async fn accordion() {
156        fn accordion_app() -> Element {
157            rsx!(
158                Accordion {
159                    summary: rsx!(AccordionSummary {
160                        label {
161                            "Accordion Summary"
162                        }
163                    }),
164                    AccordionBody {
165                        label {
166                            "Accordion Body"
167                        }
168                    }
169                }
170            )
171        }
172
173        let mut utils = launch_test(accordion_app);
174
175        let root = utils.root();
176        let content = root.get(0).get(1).get(0);
177        let label = content.get(0);
178        utils.wait_for_update().await;
179
180        // Accordion is closed, therefore label is hidden.
181        assert!(!label.is_visible());
182
183        // Click on the accordion
184        utils.click_cursor((5., 5.)).await;
185
186        // State somewhere in the middle
187        sleep(Duration::from_millis(70)).await;
188        utils.wait_for_update().await;
189
190        // Accordion is open, therefore label is visible.
191        assert!(label.is_visible());
192    }
193}