Skip to main content

freya_components/
popup.rs

1use dioxus::prelude::*;
2use freya_elements::{
3    self as dioxus_elements,
4    events::{
5        Key,
6        KeyboardEvent,
7    },
8    MouseEvent,
9};
10use freya_hooks::{
11    theme_with,
12    use_animation,
13    use_applied_theme,
14    AnimNum,
15    ButtonThemeWith,
16    Ease,
17    Function,
18    PopupTheme,
19    PopupThemeWith,
20};
21
22use crate::{
23    Button,
24    CrossIcon,
25};
26
27/// The background of the [`Popup`] component.
28#[allow(non_snake_case)]
29#[component]
30pub fn PopupBackground(children: Element, onclick: EventHandler<MouseEvent>) -> Element {
31    rsx!(rect {
32        layer: "-2000",
33        rect {
34            onclick,
35            height: "100v",
36            width: "100v",
37            background: "rgb(0, 0, 0, 150)",
38            position: "global",
39            position_top: "0",
40            position_left: "0",
41        }
42        rect {
43            height: "100v",
44            width: "100v",
45            position: "global",
46            position_top: "0",
47            position_left: "0",
48            main_align: "center",
49            cross_align: "center",
50            {children}
51        }
52    })
53}
54
55/// Floating window intended for quick interactions. Also called `Dialog` in other frameworks.
56///
57/// # Styling
58/// Inherits the [`PopupTheme`](freya_hooks::PopupTheme) theme.
59/// ```rust, no_run
60/// # use freya::prelude::*;
61/// fn app() -> Element {
62///     let mut show_popup = use_signal(|| false);
63///
64///     rsx!(
65///         if *show_popup.read() {
66///              Popup {
67///                  oncloserequest: move |_| {
68///                      show_popup.set(false)
69///                  },
70///                  PopupTitle {
71///                      label {
72///                          "Awesome Popup"
73///                      }
74///                  }
75///                  PopupContent {
76///                      label {
77///                          "Some content"
78///                      }
79///                  }
80///              }
81///          }
82///          Button {
83///              onpress: move |_| show_popup.set(true),
84///              label {
85///                  "Open"
86///              }
87///          }
88///     )
89/// }
90/// ```
91#[allow(non_snake_case)]
92#[component]
93pub fn Popup(
94    /// Theme override.
95    theme: Option<PopupThemeWith>,
96    /// Popup inner content.
97    children: Element,
98    /// Optional close request handler.
99    oncloserequest: Option<EventHandler>,
100    /// Whether to show or no the cross button in the top right corner.
101    #[props(default = true)]
102    show_close_button: bool,
103    /// Whether to trigger close request handler when the Escape key is pressed.
104    #[props(default = true)]
105    close_on_escape_key: bool,
106) -> Element {
107    let animations = use_animation(|conf| {
108        conf.auto_start(true);
109        (
110            AnimNum::new(0.85, 1.)
111                .time(150)
112                .ease(Ease::Out)
113                .function(Function::Quad),
114            AnimNum::new(40., 1.)
115                .time(150)
116                .ease(Ease::Out)
117                .function(Function::Quad),
118            AnimNum::new(0.2, 1.)
119                .time(150)
120                .ease(Ease::Out)
121                .function(Function::Quad),
122        )
123    });
124    let PopupTheme {
125        background,
126        color,
127        cross_fill,
128        width,
129        height,
130    } = use_applied_theme!(&theme, popup);
131
132    let scale = animations.get();
133    let (scale, margin, opacity) = &*scale.read();
134
135    let request_to_close = move || {
136        if let Some(oncloserequest) = &oncloserequest {
137            oncloserequest.call(());
138        }
139    };
140
141    let onglobalkeydown = move |event: KeyboardEvent| {
142        if close_on_escape_key && event.key == Key::Escape {
143            request_to_close()
144        }
145    };
146
147    rsx!(
148        PopupBackground {
149            onclick: move |_| request_to_close(),
150            rect {
151                scale: "{scale.read()} {scale.read()}",
152                margin: "{margin.read()} 0 0 0",
153                opacity: "{opacity.read()}",
154                padding: "14",
155                corner_radius: "8",
156                background: "{background}",
157                color: "{color}",
158                shadow: "0 4 5 0 rgb(0, 0, 0, 30)",
159                width: "{width}",
160                height: "{height}",
161                onglobalkeydown,
162                if show_close_button {
163                    rect {
164                        height: "0",
165                        width: "fill",
166                        cross_align: "end",
167                        Button {
168                            theme: theme_with!(ButtonTheme {
169                                padding: "6".into(),
170                                margin: "0".into(),
171                                width: "30".into(),
172                                height: "30".into(),
173                                corner_radius: "999".into(),
174                                shadow: "none".into()
175                            }),
176                            onpress: move |_| request_to_close(),
177                            CrossIcon {
178                                fill: cross_fill
179                            }
180                        }
181                    }
182                }
183                {children}
184            }
185        }
186    )
187}
188
189/// Optionally use a styled title inside a [`Popup`].
190#[allow(non_snake_case)]
191#[component]
192pub fn PopupTitle(children: Element) -> Element {
193    rsx!(
194        rect {
195            font_size: "18",
196            margin: "4 2 8 2",
197            font_weight: "bold",
198            {children}
199        }
200    )
201}
202
203/// Optionally wrap the content of your [`Popup`] in a styled container.
204#[allow(non_snake_case)]
205#[component]
206pub fn PopupContent(children: Element) -> Element {
207    rsx!(
208        rect {
209            font_size: "15",
210            margin: "6 2",
211            {children}
212        }
213    )
214}
215
216#[cfg(test)]
217mod test {
218    use std::time::Duration;
219
220    use dioxus::prelude::use_signal;
221    use freya::prelude::*;
222    use freya_elements::events::keyboard::{
223        Code,
224        Key,
225        Modifiers,
226    };
227    use freya_testing::prelude::*;
228    use tokio::time::sleep;
229
230    #[tokio::test]
231    pub async fn popup() {
232        fn popup_app() -> Element {
233            let mut show_popup = use_signal(|| false);
234
235            rsx!(
236                if *show_popup.read() {
237                    Popup {
238                        oncloserequest: move |_| {
239                            show_popup.set(false)
240                        },
241                        label {
242                            "Hello, World!"
243                        }
244                    }
245                }
246                Button {
247                    onpress: move |_| show_popup.set(true),
248                    label {
249                        "Open"
250                    }
251                }
252            )
253        }
254
255        let mut utils = launch_test(popup_app);
256        utils.wait_for_update().await;
257
258        // Check the popup is closed
259        assert_eq!(utils.sdom().get().layout().size(), 4);
260
261        // Open the popup
262        utils.click_cursor((15., 15.)).await;
263        sleep(Duration::from_millis(150)).await;
264        utils.wait_for_update().await;
265
266        // Check the popup is opened
267        assert_eq!(utils.sdom().get().layout().size(), 12);
268
269        utils.click_cursor((25., 25.)).await;
270
271        // Check the popup is closed
272        assert_eq!(utils.sdom().get().layout().size(), 4);
273
274        // Open the popup
275        utils.click_cursor((15., 15.)).await;
276
277        // Send a random globalkeydown event
278        utils.push_event(TestEvent::Keyboard {
279            name: EventName::KeyDown,
280            key: Key::ArrowDown,
281            code: Code::ArrowDown,
282            modifiers: Modifiers::empty(),
283        });
284        utils.wait_for_update().await;
285        // Check the popup is still open
286        assert_eq!(utils.sdom().get().layout().size(), 12);
287
288        // Send a ESC globalkeydown event
289        utils.push_event(TestEvent::Keyboard {
290            name: EventName::KeyDown,
291            key: Key::Escape,
292            code: Code::Escape,
293            modifiers: Modifiers::empty(),
294        });
295        utils.wait_for_update().await;
296        // Check the popup is closed
297        assert_eq!(utils.sdom().get().layout().size(), 4);
298    }
299}