leptos_shadcn_tooltip/
default.rs

1use leptos::{ev::MouseEvent, prelude::*};
2use leptos_node_ref::AnyNodeRef;
3use leptos_struct_component::{StructComponent, struct_component};
4use leptos_style::Style;
5use tailwind_fuse::*;
6
7#[derive(TwClass)]
8#[tw(
9    class = "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
10)]
11pub struct TooltipContentClass {
12    pub variant: TooltipVariant,
13}
14
15#[derive(PartialEq, TwVariant)]
16pub enum TooltipVariant {
17    #[tw(default, class = "")]
18    Default,
19}
20
21#[derive(Clone, Copy, PartialEq)]
22pub enum TooltipSide {
23    Top,
24    Right,
25    Bottom,
26    Left,
27}
28
29impl TooltipSide {
30    pub fn as_str(self) -> &'static str {
31        match self {
32            TooltipSide::Top => "top",
33            TooltipSide::Right => "right", 
34            TooltipSide::Bottom => "bottom",
35            TooltipSide::Left => "left",
36        }
37    }
38}
39
40impl std::fmt::Display for TooltipSide {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        write!(f, "{}", self.as_str())
43    }
44}
45
46impl Default for TooltipSide {
47    fn default() -> Self {
48        TooltipSide::Top
49    }
50}
51
52#[derive(Clone, StructComponent)]
53#[struct_component(tag = "div")]
54pub struct TooltipContentChildProps {
55    pub node_ref: AnyNodeRef,
56    pub class: Signal<String>,
57    pub id: MaybeProp<String>,
58    pub style: Signal<Style>,
59}
60
61#[component]
62pub fn TooltipProvider(#[prop(optional)] children: Option<Children>) -> impl IntoView {
63    children.map(|children| children())
64}
65
66#[component]  
67pub fn Tooltip(
68    #[prop(into, optional)] open: Signal<bool>,
69    #[prop(into, optional)] on_open_change: Option<Callback<bool>>,
70    #[prop(into, optional)] delay_duration: Signal<u32>,
71    #[prop(optional)] children: Option<Children>,
72) -> impl IntoView {
73    let (is_open, set_is_open) = signal(open.get_untracked());
74    
75    Effect::new(move |_| {
76        if open.get() != is_open.get() {
77            set_is_open.set(open.get());
78        }
79    });
80
81    provide_context((is_open, set_is_open, on_open_change, delay_duration));
82    
83    children.map(|children| children())
84}
85
86#[component]
87pub fn TooltipTrigger(
88    #[prop(into, optional)] as_child: Option<Callback<TooltipTriggerChildProps, AnyView>>,
89    #[prop(into, optional)] node_ref: AnyNodeRef,
90    #[prop(into, optional)] class: MaybeProp<String>,
91    #[prop(into, optional)] id: MaybeProp<String>,
92    #[prop(into, optional)] style: Signal<Style>,
93    #[prop(optional)] children: Option<Children>,
94) -> impl IntoView {
95    let (_is_open, set_is_open, on_open_change, _delay_duration) = 
96        expect_context::<(ReadSignal<bool>, WriteSignal<bool>, Option<Callback<bool>>, Signal<u32>)>();
97
98    let handle_mouse_enter = move |_: MouseEvent| {
99        set_is_open.set(true);
100        if let Some(callback) = on_open_change {
101            callback.run(true);
102        }
103    };
104
105    let handle_mouse_leave = move |_: MouseEvent| {
106        set_is_open.set(false);
107        if let Some(callback) = on_open_change {
108            callback.run(false);
109        }
110    };
111
112    let child_props = TooltipTriggerChildProps {
113        node_ref,
114        class: class.get().unwrap_or_default(),
115        id,
116        style,
117        onmouseenter: Some(Callback::new(handle_mouse_enter)),
118        onmouseleave: Some(Callback::new(handle_mouse_leave)),
119    };
120
121    if let Some(as_child) = as_child {
122        as_child.run(child_props)
123    } else {
124        child_props.render(children)
125    }
126}
127
128#[derive(Clone, StructComponent)]
129#[struct_component(tag = "div")]
130pub struct TooltipTriggerChildProps {
131    pub node_ref: AnyNodeRef,
132    pub class: String,
133    pub id: MaybeProp<String>,
134    pub style: Signal<Style>,
135    pub onmouseenter: Option<Callback<MouseEvent>>,
136    pub onmouseleave: Option<Callback<MouseEvent>>,
137}
138
139#[component]
140pub fn TooltipContent(
141    #[prop(into, optional)] _side: TooltipSide,
142    #[prop(into, optional)] _side_offset: i32,
143    #[prop(into, optional)] class: MaybeProp<String>,
144    #[prop(into, optional)] id: MaybeProp<String>,
145    #[prop(into, optional)] style: Signal<Style>,
146    #[prop(into, optional)] node_ref: AnyNodeRef,
147    #[prop(into, optional)] as_child: Option<Callback<TooltipContentChildProps, AnyView>>,
148    #[prop(optional)] children: Option<Children>,
149) -> impl IntoView {
150    let (is_open, _, _, _) = 
151        expect_context::<(ReadSignal<bool>, WriteSignal<bool>, Option<Callback<bool>>, Signal<u32>)>();
152
153    let computed_class = Memo::new(move |_| {
154        TooltipContentClass {
155            variant: TooltipVariant::Default,
156        }
157        .with_class(class.get().unwrap_or_default())
158    });
159
160    let child_props = TooltipContentChildProps {
161        node_ref,
162        class: computed_class.into(),
163        id,
164        style,
165    };
166
167    if is_open.get() {
168        if let Some(as_child) = as_child.as_ref() {
169            as_child.run(child_props.clone())
170        } else {
171            child_props.render(children)
172        }
173    } else {
174        view! { <div></div> }.into_any()
175    }
176}