impulse_thaw/tooltip/
tooltip.rs

1use leptos::{
2    ev::{self, on},
3    html,
4    leptos_dom::helpers::TimeoutHandle,
5    prelude::*,
6    tachys::html::class::class as tachys_class,
7};
8use std::time::Duration;
9use thaw_components::{Follower, FollowerPlacement};
10use thaw_utils::{class_list, mount_style};
11
12#[component]
13pub fn Tooltip<T>(
14    /// The text of the tooltip.
15    #[prop(optional, into)]
16    content: Option<Signal<String>>,
17    /// Configure the position of the tooltip.
18    #[prop(optional)]
19    position: TooltipPosition,
20    /// The tooltip's visual appearance.
21    #[prop(optional, into)]
22    appearance: Signal<TooltipAppearance>,
23    children: TypedChildren<T>,
24) -> impl IntoView
25where
26    T: AddAnyAttr + IntoView + Send + 'static,
27{
28    mount_style("tooltip", include_str!("./tooltip.css"));
29
30    let is_show_content = RwSignal::new(false);
31    let content_handle = StoredValue::new(None::<TimeoutHandle>);
32
33    let on_mouse_enter = move |_| {
34        content_handle.update_value(|handle| {
35            if let Some(handle) = handle.take() {
36                handle.clear();
37            }
38        });
39        is_show_content.set(true);
40    };
41    let on_mouse_leave = move |_| {
42        content_handle.update_value(|handle| {
43            if let Some(handle) = handle.take() {
44                handle.clear();
45            }
46            *handle = set_timeout_with_handle(
47                move || {
48                    is_show_content.set(false);
49                },
50                Duration::from_millis(100),
51            )
52            .ok();
53        });
54    };
55
56    let arrow_ref = NodeRef::<html::Div>::new();
57    let edge_length = 1.414 * 8.0;
58    let arrow_style = format!(
59        "--thaw-positioning-arrow-height: {}px; --thaw-positioning-arrow-offset: {}px;",
60        edge_length,
61        (edge_length / 2.0) * -1.0
62    );
63
64    Owner::on_cleanup(move || {
65        content_handle.update_value(|handle| {
66            if let Some(handle) = handle.take() {
67                handle.clear();
68            }
69        });
70    });
71
72    view! {
73        <crate::_binder::Binder>
74            {children
75                .into_inner()()
76                .into_inner()
77                .add_any_attr(tachys_class(("thaw-tooltip", true)))
78                .add_any_attr(on(ev::mouseenter, on_mouse_enter))
79                .add_any_attr(on(ev::mouseleave, on_mouse_leave))}
80            <Follower slot show=is_show_content placement=position arrow=(edge_length / 2.0 + 2.0, arrow_ref)>
81                <div
82                    class=class_list![
83                        "thaw-tooltip-content",
84                         move || format!("thaw-tooltip-content--{}", appearance.get().as_str())
85                    ]
86                    role="tooltip"
87                    on:mouseenter=on_mouse_enter
88                    on:mouseleave=on_mouse_leave
89                >
90                    {move || { content.as_ref().map(|c| c.get()).unwrap_or_default() }}
91                    <div class="thaw-tooltip-content__angle" style=arrow_style node_ref=arrow_ref></div>
92                </div>
93            </Follower>
94        </crate::_binder::Binder>
95    }
96}
97
98#[derive(Clone, Default)]
99pub enum TooltipAppearance {
100    #[default]
101    Normal,
102    Inverted,
103}
104
105impl TooltipAppearance {
106    pub fn as_str(&self) -> &'static str {
107        match self {
108            Self::Normal => "normal",
109            Self::Inverted => "inverted",
110        }
111    }
112}
113
114#[derive(Default)]
115pub enum TooltipPosition {
116    #[default]
117    Top,
118    Bottom,
119    Left,
120    Right,
121    TopStart,
122    TopEnd,
123    LeftStart,
124    LeftEnd,
125    RightStart,
126    RightEnd,
127    BottomStart,
128    BottomEnd,
129}
130
131impl From<TooltipPosition> for FollowerPlacement {
132    fn from(value: TooltipPosition) -> Self {
133        match value {
134            TooltipPosition::Top => Self::Top,
135            TooltipPosition::Bottom => Self::Bottom,
136            TooltipPosition::Left => Self::Left,
137            TooltipPosition::Right => Self::Right,
138            TooltipPosition::TopStart => Self::TopStart,
139            TooltipPosition::TopEnd => Self::TopEnd,
140            TooltipPosition::LeftStart => Self::LeftStart,
141            TooltipPosition::LeftEnd => Self::LeftEnd,
142            TooltipPosition::RightStart => Self::RightStart,
143            TooltipPosition::RightEnd => Self::RightEnd,
144            TooltipPosition::BottomStart => Self::BottomStart,
145            TooltipPosition::BottomEnd => Self::BottomEnd,
146        }
147    }
148}