1use 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::polygon::{get_points_from_el, make_hull, point_in_polygon},
14};
15
16#[derive(Copy, Clone)]
17pub enum Positioning {
18 Top,
19 TopStart,
20 TopEnd,
21 Right,
22 RightStart,
23 RightEnd,
24 Bottom,
25 BottomStart,
26 BottomEnd,
27 Left,
28 LeftStart,
29 LeftEnd,
30}
31
32impl Default for Positioning {
33 fn default() -> Self {
34 Positioning::Top
35 }
36}
37
38impl Positioning {
39 pub fn calculate_position_style(
40 self,
41 top: f64,
42 left: f64,
43 width: f64,
44 height: f64,
45 content_height: f64,
46 content_width: f64,
47 arrow_size: f64,
48 ) -> String {
49 let position = self.calculate_position(
50 top,
51 left,
52 width,
53 height,
54 content_height,
55 content_width,
56 arrow_size,
57 );
58 let arrow_position = self.calculate_arrow_position(top, left, width, height, arrow_size);
59 format!(
60 "position: fixed; top: {}px; left: {}px; --biji-tooltip-arrow-top: {}px; --biji-tooltip-arrow-left: {}px; --biji-tooltip-arrow-rotation: {}deg;",
61 position.0, position.1, arrow_position.0, arrow_position.1, arrow_position.2
62 )
63 }
64
65 pub fn calculate_position(
66 self,
67 top: f64,
68 left: f64,
69 width: f64,
70 height: f64,
71 content_height: f64,
72 content_width: f64,
73 arrow_size: f64,
74 ) -> (f64, f64) {
75 match self {
76 Positioning::Top => {
77 let top = top - content_height - arrow_size;
78 let left = left + (width / 2.0) - (content_width / 2.0);
79 (top, left)
80 }
81 Positioning::TopStart => {
82 let top = top - content_height - arrow_size;
83 (top, left)
84 }
85 Positioning::TopEnd => {
86 let top = top - content_height - arrow_size;
87 let left = left + width - content_width;
88 (top, left)
89 }
90 Positioning::Right => {
91 let top = top + (height / 2.0) - (content_height / 2.0);
92 let left = left + width + arrow_size;
93 (top, left)
94 }
95 Positioning::RightStart => {
96 let left = left + width + arrow_size;
97 (top, left)
98 }
99 Positioning::RightEnd => {
100 let top = top + height - content_height;
101 let left = left + width + arrow_size;
102 (top, left)
103 }
104 Positioning::Bottom => {
105 let top = top + height + arrow_size;
106 let left = left + (width / 2.0) - (content_width / 2.0);
107 (top, left)
108 }
109 Positioning::BottomStart => {
110 let top = top + height + arrow_size;
111 (top, left)
112 }
113 Positioning::BottomEnd => {
114 let top = top + height + arrow_size;
115 let left = left + width - content_width;
116 (top, left)
117 }
118 Positioning::Left => {
119 let top = top + (height / 2.0) - (content_height / 2.0);
120 let left = left - content_width - arrow_size;
121 (top, left)
122 }
123 Positioning::LeftStart => {
124 let left = left - content_width - arrow_size;
125 (top, left)
126 }
127 Positioning::LeftEnd => {
128 let left = left - content_width - arrow_size;
129 let top = top + height - content_height;
130 (top, left)
131 }
132 }
133 }
134
135 pub fn calculate_arrow_position(
136 self,
137 top: f64,
138 left: f64,
139 width: f64,
140 height: f64,
141 arrow_size: f64,
142 ) -> (f64, f64, i32) {
143 match self {
144 Positioning::Top => {
145 let top = top - arrow_size - (arrow_size / 2.0);
146 let left = left + (width / 2.0) - (arrow_size / 2.0);
147 (top, left, 225)
148 }
149 Positioning::TopStart => {
150 let top = top - arrow_size - (arrow_size / 2.0);
151 let left = left + (width / 2.0) - (arrow_size / 2.0);
152 (top, left, 225)
153 }
154 Positioning::TopEnd => {
155 let top = top - arrow_size - (arrow_size / 2.0);
156 let left = left + (width / 2.0) - (arrow_size / 2.0);
157 (top, left, 225)
158 }
159 Positioning::Right => {
160 let top = top + (height / 2.0) - (arrow_size / 2.0);
161 let left = left + width + (arrow_size / 2.0);
162 (top, left, 315)
163 }
164 Positioning::RightStart => {
165 let top = top + (height / 2.0) - (arrow_size / 2.0);
166 let left = left + width + (arrow_size / 2.0);
167 (top, left, 315)
168 }
169 Positioning::RightEnd => {
170 let top = top + (height / 2.0) - (arrow_size / 2.0);
171 let left = left + width + (arrow_size / 2.0);
172 (top, left, 315)
173 }
174 Positioning::Bottom => {
175 let top = top + height + arrow_size - (arrow_size / 2.0);
176 let left = left + (width / 2.0) - (arrow_size / 2.0);
177 (top, left, 45)
178 }
179 Positioning::BottomStart => {
180 let top = top + height + arrow_size - (arrow_size / 2.0);
181 let left = left + (width / 2.0) - (arrow_size / 2.0);
182 (top, left, 45)
183 }
184 Positioning::BottomEnd => {
185 let top = top + height + arrow_size - (arrow_size / 2.0);
186 let left = left + (width / 2.0) - (arrow_size / 2.0);
187 (top, left, 45)
188 }
189 Positioning::Left => {
190 let top = top + (height / 2.0) - (arrow_size / 2.0);
191 let left = left - arrow_size - (arrow_size / 2.0);
192 (top, left, 135)
193 }
194 Positioning::LeftStart => {
195 let top = top + (height / 2.0) - (arrow_size / 2.0);
196 let left = left - arrow_size - (arrow_size / 2.0);
197 (top, left, 135)
198 }
199 Positioning::LeftEnd => {
200 let top = top + (height / 2.0) - (arrow_size / 2.0);
201 let left = left - arrow_size - (arrow_size / 2.0);
202 (top, left, 135)
203 }
204 }
205 }
206}
207
208#[component]
209pub fn Trigger(children: Children, #[prop(into, optional)] class: String) -> impl IntoView {
210 let tooltip_ctx = expect_context::<TooltipContext>();
211
212 let trigger_ref = tooltip_ctx.trigger_ref;
213
214 view! {
215 <TriggerEvents>
216 <button node_ref={trigger_ref} class={class}>
217 {children()}
218 </button>
219 </TriggerEvents>
220 }
221}
222
223#[component]
224pub fn TriggerEvents(children: Children) -> impl IntoView {
225 let tooltip_ctx = expect_context::<TooltipContext>();
226
227 let UseElementBoundingReturn {
228 top: trigger_top,
229 left: trigger_left,
230 bottom: trigger_bottom,
231 right: trigger_right,
232 width: trigger_width,
233 height: trigger_height,
234 ..
235 } = use_element_bounding(tooltip_ctx.trigger_ref);
236
237 let UseElementBoundingReturn {
238 width: content_width,
239 height: content_height,
240 ..
241 } = use_element_bounding(tooltip_ctx.content_ref);
242
243 let polygon_elements = move || {
244 let mut trigger_points =
245 get_points_from_el(&(trigger_top, trigger_right, trigger_bottom, trigger_left));
246
247 let content_pos = tooltip_ctx.positioning.calculate_position(
248 trigger_top.get(),
249 trigger_left.get(),
250 trigger_width.get(),
251 trigger_height.get(),
252 content_height.get(),
253 content_width.get(),
254 tooltip_ctx.arrow_size as f64,
255 );
256
257 let mut content_points = vec![
258 (content_pos.1, content_pos.0),
259 (content_pos.1 + content_width.get(), content_pos.0),
260 (
261 content_pos.1 + content_width.get(),
262 content_pos.0 + content_height.get(),
263 ),
264 (content_pos.1, content_pos.0 + content_height.get()),
265 ];
266
267 trigger_points.append(&mut content_points);
268
269 trigger_points
270 };
271
272 let polygon = move || make_hull(&polygon_elements());
273
274 let _ = use_event_listener(tooltip_ctx.trigger_ref, pointerenter, move |_| {
275 tooltip_ctx.pointer_inside_trigger.set(true);
276 tooltip_ctx.open();
277 });
278
279 let _ = use_event_listener(tooltip_ctx.trigger_ref, pointerleave, move |_| {
280 tooltip_ctx.pointer_inside_trigger.set(false);
281 });
282
283 let _ = use_event_listener(tooltip_ctx.content_ref, pointerenter, move |_| {
284 tooltip_ctx.pointer_inside_content.set(true);
285 });
286
287 let _ = use_event_listener(tooltip_ctx.content_ref, pointerleave, move |_| {
288 tooltip_ctx.pointer_inside_content.set(false);
289 });
290
291 let _ = use_event_listener(use_document(), mousemove, move |e| {
292 if tooltip_ctx.pointer_inside_content.get()
293 || tooltip_ctx.pointer_inside_trigger.get()
294 || point_in_polygon((e.x() as f64, e.y() as f64), &polygon())
295 {
296 return;
297 }
298 if tooltip_ctx.open.get() {
299 tooltip_ctx.close();
300 }
301 });
302
303 let _ = use_event_listener(tooltip_ctx.trigger_ref, focus, move |_| {
304 tooltip_ctx.open();
305 });
306
307 let _ = use_event_listener(tooltip_ctx.trigger_ref, blur, move |_| {
308 if tooltip_ctx.open.get() {
309 tooltip_ctx.close();
310 }
311 });
312
313 children()
314}
315
316#[component]
317pub fn Root(
318 #[prop(into, optional)] class: String,
319 children: Children,
320 #[prop(default = Duration::from_millis(200))]
322 hide_delay: Duration,
323 #[prop(default = Positioning::default())] positioning: Positioning,
324) -> impl IntoView {
325 let ctx = TooltipContext {
326 hide_delay,
327 positioning,
328 ..TooltipContext::default()
329 };
330
331 view! {
332 <Provider value={ctx}>
333 <div class={class}>{children()}</div>
334 </Provider>
335 }
336}
337
338#[component]
339pub fn Content(
340 children: ChildrenFn,
341 #[prop(into, optional)]
343 class: String,
344 #[prop(into, optional)]
346 show_class: String,
347 #[prop(into, optional)]
349 hide_class: String,
350) -> impl IntoView {
351 let show_class = cn!(class, show_class);
352 let hide_class = cn!(class, hide_class);
353
354 let tooltip_ctx = expect_context::<TooltipContext>();
355
356 let content_ref = tooltip_ctx.content_ref;
357
358 let hide_delay = tooltip_ctx.hide_delay;
359 let when = tooltip_ctx.open;
360
361 let show_handle: StoredValue<Option<TimeoutHandle>> = StoredValue::new(None);
362 let hide_handle: StoredValue<Option<TimeoutHandle>> = StoredValue::new(None);
363 let cls = RwSignal::new(if when.get_untracked() {
364 show_class.clone()
365 } else {
366 hide_class.clone()
367 });
368 let show = RwSignal::new(when.get_untracked());
369
370 let UseElementBoundingReturn {
371 width: content_width,
372 height: content_height,
373 ..
374 } = use_element_bounding(tooltip_ctx.content_ref);
375
376 let UseElementBoundingReturn {
377 top,
378 left,
379 width,
380 height,
381 ..
382 } = use_element_bounding(tooltip_ctx.trigger_ref);
383
384 let style = move || {
385 tooltip_ctx.positioning.calculate_position_style(
386 *top.read(),
387 *left.read(),
388 *width.read(),
389 *height.read(),
390 *content_height.read(),
391 *content_width.read(),
392 tooltip_ctx.arrow_size as f64,
393 )
394 };
395
396 let eff = RenderEffect::new(move |_| {
397 let show_class = show_class.clone();
398 if when.get() {
399 if let Some(h) = show_handle.get_value() {
401 h.clear();
402 }
403 if let Some(h) = hide_handle.get_value() {
404 h.clear();
405 }
406
407 let h = leptos_dom::helpers::set_timeout_with_handle(
408 move || cls.set(show_class.clone()),
409 Duration::from_millis(1),
410 )
411 .expect("set timeout in AnimatedShow");
412 show_handle.set_value(Some(h));
413
414 cls.set(hide_class.clone());
415 show.set(true);
416 } else {
417 cls.set(hide_class.clone());
418
419 let h =
420 leptos_dom::helpers::set_timeout_with_handle(move || show.set(false), hide_delay)
421 .expect("set timeout in AnimatedShow");
422 hide_handle.set_value(Some(h));
423 }
424 });
425
426 on_cleanup(move || {
427 if let Some(Some(h)) = show_handle.try_get_value() {
428 h.clear();
429 }
430 if let Some(Some(h)) = hide_handle.try_get_value() {
431 h.clear();
432 }
433 drop(eff);
434 });
435
436 view! {
437 <Show when={move || show.get()} fallback={|| ()}>
438 <div node_ref={content_ref} class={move || cls.get()} style={style}>
439 {children()}
440 </div>
441 </Show>
442 }
443}
444
445#[component]
446pub fn Arrow(#[prop(into, optional)] class: String) -> impl IntoView {
447 let tooltip_ctx = expect_context::<TooltipContext>();
448 view! {
449 <div
450 class={class}
451 style={move || {
452 format!(
453 "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));",
454 tooltip_ctx.arrow_size,
455 tooltip_ctx.arrow_size,
456 )
457 }}
458 >
459 </div>
460 }
461}