biji_ui/components/tooltip/
tooltip.rs1use std::time::Duration;
2
3use leptos::ev::{blur, focus, mousemove, pointerenter, pointerleave};
4use leptos::{context::Provider, leptos_dom, prelude::*};
5use leptos_dom::helpers::TimeoutHandle;
6use leptos_use::{
7 use_document, use_element_bounding, use_event_listener, UseElementBoundingReturn,
8};
9
10use crate::{
11 cn,
12 components::tooltip::context::TooltipContext,
13 utils::{
14 polygon::{get_points_from_el, make_hull, point_in_polygon},
15 positioning::Positioning,
16 },
17};
18
19#[component]
20pub fn Trigger(children: Children, #[prop(into, optional)] class: String) -> impl IntoView {
21 let tooltip_ctx = expect_context::<TooltipContext>();
22
23 let trigger_ref = tooltip_ctx.trigger_ref;
24
25 view! {
26 <TriggerEvents>
27 <button node_ref={trigger_ref} class={class}>
28 {children()}
29 </button>
30 </TriggerEvents>
31 }
32}
33
34#[component]
35pub fn TriggerEvents(children: Children) -> impl IntoView {
36 let tooltip_ctx = expect_context::<TooltipContext>();
37
38 let UseElementBoundingReturn {
39 top: trigger_top,
40 left: trigger_left,
41 bottom: trigger_bottom,
42 right: trigger_right,
43 width: trigger_width,
44 height: trigger_height,
45 ..
46 } = use_element_bounding(tooltip_ctx.trigger_ref);
47
48 let UseElementBoundingReturn {
49 width: content_width,
50 height: content_height,
51 ..
52 } = use_element_bounding(tooltip_ctx.content_ref);
53
54 let polygon_elements = move || {
55 let mut trigger_points =
56 get_points_from_el(&(trigger_top, trigger_right, trigger_bottom, trigger_left));
57
58 let content_pos = tooltip_ctx.positioning.calculate_position(
59 trigger_top.get(),
60 trigger_left.get(),
61 trigger_width.get(),
62 trigger_height.get(),
63 content_height.get(),
64 content_width.get(),
65 tooltip_ctx.arrow_size as f64,
66 );
67
68 let mut content_points = vec![
69 (content_pos.1, content_pos.0),
70 (content_pos.1 + content_width.get(), content_pos.0),
71 (
72 content_pos.1 + content_width.get(),
73 content_pos.0 + content_height.get(),
74 ),
75 (content_pos.1, content_pos.0 + content_height.get()),
76 ];
77
78 trigger_points.append(&mut content_points);
79
80 trigger_points
81 };
82
83 let polygon = move || make_hull(&polygon_elements());
84
85 let _ = use_event_listener(tooltip_ctx.trigger_ref, pointerenter, move |_| {
86 tooltip_ctx.pointer_inside_trigger.set(true);
87 tooltip_ctx.open();
88 });
89
90 let _ = use_event_listener(tooltip_ctx.trigger_ref, pointerleave, move |_| {
91 tooltip_ctx.pointer_inside_trigger.set(false);
92 });
93
94 let _ = use_event_listener(tooltip_ctx.content_ref, pointerenter, move |_| {
95 tooltip_ctx.pointer_inside_content.set(true);
96 });
97
98 let _ = use_event_listener(tooltip_ctx.content_ref, pointerleave, move |_| {
99 tooltip_ctx.pointer_inside_content.set(false);
100 });
101
102 let _ = use_event_listener(use_document(), mousemove, move |e| {
103 if tooltip_ctx.pointer_inside_content.get()
104 || tooltip_ctx.pointer_inside_trigger.get()
105 || point_in_polygon((e.x() as f64, e.y() as f64), &polygon())
106 {
107 return;
108 }
109 if tooltip_ctx.open.get() {
110 tooltip_ctx.close();
111 }
112 });
113
114 let _ = use_event_listener(tooltip_ctx.trigger_ref, focus, move |_| {
115 tooltip_ctx.open();
116 });
117
118 let _ = use_event_listener(tooltip_ctx.trigger_ref, blur, move |_| {
119 if tooltip_ctx.open.get() {
120 tooltip_ctx.close();
121 }
122 });
123
124 children()
125}
126
127#[component]
128pub fn Root(
129 #[prop(into, optional)] class: String,
130 children: Children,
131 #[prop(default = Duration::from_millis(200))]
133 hide_delay: Duration,
134 #[prop(default = Positioning::default())] positioning: Positioning,
135) -> impl IntoView {
136 let ctx = TooltipContext {
137 hide_delay,
138 positioning,
139 ..TooltipContext::default()
140 };
141
142 view! {
143 <Provider value={ctx}>
144 <div class={class}>{children()}</div>
145 </Provider>
146 }
147}
148
149#[component]
150pub fn Content(
151 children: ChildrenFn,
152 #[prop(into, optional)]
154 class: String,
155 #[prop(into, optional)]
157 show_class: String,
158 #[prop(into, optional)]
160 hide_class: String,
161) -> impl IntoView {
162 let show_class = cn!(class, show_class);
163 let hide_class = cn!(class, hide_class);
164
165 let tooltip_ctx = expect_context::<TooltipContext>();
166
167 let content_ref = tooltip_ctx.content_ref;
168
169 let hide_delay = tooltip_ctx.hide_delay;
170 let when = tooltip_ctx.open;
171
172 let show_handle: StoredValue<Option<TimeoutHandle>> = StoredValue::new(None);
173 let hide_handle: StoredValue<Option<TimeoutHandle>> = StoredValue::new(None);
174 let cls = RwSignal::new(if when.get_untracked() {
175 show_class.clone()
176 } else {
177 hide_class.clone()
178 });
179 let show = RwSignal::new(when.get_untracked());
180
181 let UseElementBoundingReturn {
182 width: content_width,
183 height: content_height,
184 ..
185 } = use_element_bounding(tooltip_ctx.content_ref);
186
187 let UseElementBoundingReturn {
188 top,
189 left,
190 width,
191 height,
192 ..
193 } = use_element_bounding(tooltip_ctx.trigger_ref);
194
195 let style = move || {
196 tooltip_ctx.positioning.calculate_position_style(
197 *top.read(),
198 *left.read(),
199 *width.read(),
200 *height.read(),
201 *content_height.read(),
202 *content_width.read(),
203 tooltip_ctx.arrow_size as f64,
204 tooltip_ctx.arrow_size as f64,
205 )
206 };
207
208 let eff = RenderEffect::new(move |_| {
209 let show_class = show_class.clone();
210 if when.get() {
211 if let Some(h) = show_handle.get_value() {
213 h.clear();
214 }
215 if let Some(h) = hide_handle.get_value() {
216 h.clear();
217 }
218
219 let h = leptos_dom::helpers::set_timeout_with_handle(
220 move || cls.set(show_class.clone()),
221 Duration::from_millis(1),
222 )
223 .expect("set timeout in AnimatedShow");
224 show_handle.set_value(Some(h));
225
226 cls.set(hide_class.clone());
227 show.set(true);
228 } else {
229 cls.set(hide_class.clone());
230
231 let h =
232 leptos_dom::helpers::set_timeout_with_handle(move || show.set(false), hide_delay)
233 .expect("set timeout in AnimatedShow");
234 hide_handle.set_value(Some(h));
235 }
236 });
237
238 on_cleanup(move || {
239 if let Some(Some(h)) = show_handle.try_get_value() {
240 h.clear();
241 }
242 if let Some(Some(h)) = hide_handle.try_get_value() {
243 h.clear();
244 }
245 drop(eff);
246 });
247
248 view! {
249 <Show when={move || show.get()} fallback={|| ()}>
250 <div node_ref={content_ref} class={move || cls.get()} style={style}>
251 {children()}
252 </div>
253 </Show>
254 }
255}
256
257#[component]
258pub fn Arrow(#[prop(into, optional)] class: String) -> impl IntoView {
259 let tooltip_ctx = expect_context::<TooltipContext>();
260 view! {
261 <div
262 class={class}
263 style={move || {
264 format!(
265 "position: fixed; top: var(--biji-tooltip-arrow-top); left: var(--biji-tooltip-arrow-left); height: {}px; width: {}px; background-color: inherit; transform: rotate(var(--biji-tooltip-arrow-rotation));",
266 tooltip_ctx.arrow_size,
267 tooltip_ctx.arrow_size,
268 )
269 }}
270 ></div>
271 }
272}