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}