impulse_thaw/popover/
mod.rs1mod 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 #[prop(optional)]
22 trigger_type: PopoverTriggerType,
23 popover_trigger: PopoverTrigger<T>,
25 #[prop(optional)]
27 position: PopoverPosition,
28 #[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}