Skip to main content

impulse_thaw/popover/
mod.rs

1mod types;
2
3pub use types::*;
4
5use leptos::{
6    either::Either,
7    ev::{self, on},
8    html,
9    leptos_dom::helpers::TimeoutHandle,
10    prelude::*,
11    tachys::html::{class::class as tachys_class, node_ref::node_ref},
12};
13use std::time::Duration;
14use thaw_components::Follower;
15use thaw_utils::{class_list, mount_style, on_click_outside, BoxCallback};
16
17#[component]
18pub fn Popover<T>(
19    #[prop(optional, into)] class: MaybeProp<String>,
20    /// Action that displays the popover.
21    #[prop(optional)]
22    trigger_type: PopoverTriggerType,
23    /// The element or component that triggers popover.
24    popover_trigger: PopoverTrigger<T>,
25    /// Configures the position of the Popover.
26    #[prop(optional)]
27    position: PopoverPosition,
28    /// A popover can appear styled with brand or inverted.
29    /// When not specified, the default style is used.
30    #[prop(optional, into)]
31    appearance: MaybeProp<PopoverAppearance>,
32    #[prop(optional, into)] size: Signal<PopoverSize>,
33    #[prop(optional, into)] on_open: Option<BoxCallback>,
34    #[prop(optional, into)] on_close: Option<BoxCallback>,
35    children: Children,
36) -> impl IntoView
37where
38    T: AddAnyAttr + IntoView + Send + 'static,
39{
40    mount_style("popover", include_str!("./popover.css"));
41
42    let popover_ref = NodeRef::<html::Div>::new();
43    let is_show_popover = RwSignal::new(false);
44    let show_popover_handle = StoredValue::new(None::<TimeoutHandle>);
45
46    if on_open.is_some() || on_close.is_some() {
47        Effect::watch(
48            move || is_show_popover.get(),
49            move |is_shown, prev_is_shown, _| {
50                if prev_is_shown != Some(is_shown) {
51                    if *is_shown {
52                        if let Some(on_open) = &on_open {
53                            on_open();
54                        }
55                    } else {
56                        if let Some(on_close) = &on_close {
57                            on_close();
58                        }
59                    }
60                }
61            },
62            false,
63        );
64    }
65
66    let on_mouse_enter = move |_| {
67        if trigger_type != PopoverTriggerType::Hover {
68            return;
69        }
70        show_popover_handle.update_value(|handle| {
71            if let Some(handle) = handle.take() {
72                handle.clear();
73            }
74        });
75        is_show_popover.set(true);
76    };
77    let on_mouse_leave = move |_| {
78        if trigger_type != PopoverTriggerType::Hover {
79            return;
80        }
81        show_popover_handle.update_value(|handle| {
82            if let Some(handle) = handle.take() {
83                handle.clear();
84            }
85            *handle = set_timeout_with_handle(
86                move || {
87                    is_show_popover.set(false);
88                },
89                Duration::from_millis(100),
90            )
91            .ok();
92        });
93    };
94
95    let PopoverTrigger {
96        children: trigger_children,
97    } = popover_trigger;
98    let trigger_children = trigger_children.into_inner()()
99        .into_inner()
100        .add_any_attr(tachys_class(("thaw-popover-trigger", true)))
101        .add_any_attr(tachys_class(("thaw-popover-trigger--open", move || {
102            is_show_popover.get()
103        })));
104
105    let trigger_children = match trigger_type {
106        PopoverTriggerType::Click => {
107            let trigger_ref = NodeRef::<thaw_utils::Element>::new();
108            on_click_outside(
109                move || {
110                    if !is_show_popover.get_untracked() {
111                        return None;
112                    }
113                    let Some(trigger_el) = trigger_ref.get_untracked() else {
114                        return None;
115                    };
116                    let Some(popover_el) = popover_ref.get_untracked() else {
117                        return None;
118                    };
119                    Some(vec![popover_el.into(), trigger_el])
120                },
121                move || is_show_popover.set(false),
122            );
123            Either::Left(
124                trigger_children
125                    .add_any_attr(node_ref(trigger_ref))
126                    .add_any_attr(on(ev::click, move |_| {
127                        is_show_popover.update(|show| {
128                            *show = !*show;
129                        });
130                    })),
131            )
132        }
133        PopoverTriggerType::Hover => Either::Right(
134            trigger_children
135                .add_any_attr(on(ev::mouseenter, on_mouse_enter))
136                .add_any_attr(on(ev::mouseleave, on_mouse_leave)),
137        ),
138    };
139
140    let arrow_ref = NodeRef::<html::Div>::new();
141    let edge_length = 1.414 * 8.0;
142    let arrow_style = format!(
143        "--thaw-positioning-arrow-height: {}px; --thaw-positioning-arrow-offset: {}px;",
144        edge_length,
145        (edge_length / 2.0) * -1.0
146    );
147
148    view! {
149        <crate::_binder::Binder>
150            {trigger_children} <Follower slot show=is_show_popover placement=position arrow=(edge_length / 2.0 + 2.0, arrow_ref)>
151                <div
152                    class=class_list![
153                        "thaw-popover-surface",
154                        move || format!("thaw-popover-surface--{}", size.get().as_str()),
155                        move || appearance.get().map(|a| format!("thaw-popover-surface--{}", a.as_str())),
156                        class
157                    ]
158
159                    node_ref=popover_ref
160                    on:mouseenter=on_mouse_enter
161                    on:mouseleave=on_mouse_leave
162                >
163                    {children()}
164                    <div class="thaw-popover-surface__angle" style=arrow_style node_ref=arrow_ref></div>
165                </div>
166            </Follower>
167        </crate::_binder::Binder>
168    }
169}