1use crate::components::floating::use_floating_close_handle;
2use crate::components::overlay::OverlayKind;
3use crate::components::select_base::use_floating_layer;
4use dioxus::events::{KeyboardEvent, MouseEvent};
5use dioxus::prelude::Key;
6use dioxus::prelude::*;
7
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
10pub enum TooltipPlacement {
11 #[default]
12 Top,
13 Bottom,
14 Left,
15 Right,
16}
17
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
20pub enum TooltipTrigger {
21 #[default]
22 Hover,
23 Click,
24}
25
26#[derive(Props, Clone, PartialEq)]
28pub struct TooltipProps {
29 #[props(optional)]
31 pub title: Option<String>,
32 #[props(optional)]
34 pub content: Option<Element>,
35 #[props(optional)]
37 pub placement: Option<TooltipPlacement>,
38 #[props(default)]
40 pub trigger: TooltipTrigger,
41 #[props(optional)]
44 pub open: Option<bool>,
45 #[props(optional)]
47 pub default_open: Option<bool>,
48 #[props(optional)]
50 pub on_open_change: Option<EventHandler<bool>>,
51 #[props(default)]
53 pub disabled: bool,
54 #[props(optional)]
56 pub class: Option<String>,
57 #[props(optional)]
59 pub overlay_class: Option<String>,
60 #[props(optional)]
62 pub overlay_style: Option<String>,
63 pub children: Element,
65}
66
67#[component]
69pub fn Tooltip(props: TooltipProps) -> Element {
70 let TooltipProps {
71 title,
72 content,
73 placement,
74 trigger,
75 open,
76 default_open,
77 on_open_change,
78 disabled,
79 class,
80 overlay_class,
81 overlay_style,
82 children,
83 } = props;
84
85 let placement = placement.unwrap_or_default();
86
87 let open_state: Signal<bool> = use_signal(|| default_open.unwrap_or(false));
89 let is_controlled = open.is_some();
90 let current_open = open.unwrap_or(*open_state.read());
91
92 let floating = use_floating_layer(OverlayKind::Tooltip, current_open);
94 let current_z = *floating.z_index.read();
95
96 let close_handle = if !is_controlled && matches!(trigger, TooltipTrigger::Click) {
100 Some(use_floating_close_handle(open_state))
101 } else {
102 None
103 };
104
105 let disabled_flag = disabled;
107 let is_controlled_flag = is_controlled;
108 let open_for_handlers = open_state;
109 let trigger_mode = trigger;
110 let current_open_flag = current_open;
111 let close_handle_for_click = close_handle;
112 let close_handle_for_content = close_handle;
113
114 let class_attr = {
115 let mut list = vec!["adui-tooltip-root".to_string()];
116 if let Some(extra) = class {
117 list.push(extra);
118 }
119 list.join(" ")
120 };
121
122 let overlay_class_attr = {
123 let mut list = vec!["adui-tooltip".to_string()];
124 if let Some(extra) = overlay_class {
125 list.push(extra);
126 }
127 list.join(" ")
128 };
129
130 let overlay_style_attr = {
131 let placement_css = match placement {
132 TooltipPlacement::Top => {
133 "bottom: 100%; left: 50%; transform: translateX(-50%); margin-bottom: 8px;"
134 }
135 TooltipPlacement::Bottom => {
136 "top: 100%; left: 50%; transform: translateX(-50%); margin-top: 8px;"
137 }
138 TooltipPlacement::Left => {
139 "right: 100%; top: 50%; transform: translateY(-50%); margin-right: 8px;"
140 }
141 TooltipPlacement::Right => {
142 "left: 100%; top: 50%; transform: translateY(-50%); margin-left: 8px;"
143 }
144 };
145 let extra = overlay_style.unwrap_or_default();
146 format!(
147 "position: absolute; z-index: {}; {}; {}",
148 current_z, placement_css, extra
149 )
150 };
151
152 let title_text = title.clone();
153 let content_node = content.clone();
154
155 rsx! {
156 span {
157 class: "{class_attr}",
158 style: "position: relative; display: inline-block;",
159 onmouseenter: move |_evt: MouseEvent| {
160 if matches!(trigger_mode, TooltipTrigger::Hover) {
161 update_open_state(
162 disabled_flag,
163 is_controlled_flag,
164 open_for_handlers,
165 on_open_change,
166 true,
167 );
168 }
169 },
170 onmouseleave: move |_evt: MouseEvent| {
171 if matches!(trigger_mode, TooltipTrigger::Hover) {
172 update_open_state(
173 disabled_flag,
174 is_controlled_flag,
175 open_for_handlers,
176 on_open_change,
177 false,
178 );
179 }
180 },
181 onclick: move |_evt: MouseEvent| {
182 if !matches!(trigger_mode, TooltipTrigger::Click) {
183 return;
184 }
185 if let Some(handle) = close_handle_for_click {
186 handle.mark_internal_click();
187 }
188 update_open_state(
189 disabled_flag,
190 is_controlled_flag,
191 open_for_handlers,
192 on_open_change,
193 !current_open_flag,
194 );
195 },
196 {children}
197 if current_open {
198 div {
199 class: "{overlay_class_attr}",
200 style: "{overlay_style_attr}",
201 tabindex: 0,
202 onkeydown: move |evt: KeyboardEvent| {
203 if matches!(evt.key(), Key::Escape) {
204 evt.prevent_default();
205 update_open_state(
206 disabled_flag,
207 is_controlled_flag,
208 open_for_handlers,
209 on_open_change,
210 false,
211 );
212 }
213 },
214 onclick: move |_evt| {
215 if let Some(handle) = close_handle_for_content {
216 handle.mark_internal_click();
217 }
218 },
219 div { class: "adui-tooltip-inner",
220 if let Some(node) = content_node {
221 {node}
222 } else if let Some(text) = title_text {
223 span { "{text}" }
224 }
225 }
226 }
227 }
228 }
229 }
230}
231
232pub fn update_open_state(
233 disabled: bool,
234 is_controlled: bool,
235 mut open_signal: Signal<bool>,
236 on_open_change: Option<EventHandler<bool>>,
237 next: bool,
238) {
239 if disabled {
240 return;
241 }
242
243 if is_controlled {
244 if let Some(cb) = on_open_change {
245 cb.call(next);
246 }
247 } else {
248 open_signal.set(next);
249 if let Some(cb) = on_open_change {
250 cb.call(next);
251 }
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn tooltip_placement_default() {
261 assert_eq!(TooltipPlacement::default(), TooltipPlacement::Top);
262 }
263
264 #[test]
265 fn tooltip_placement_all_variants() {
266 assert_eq!(TooltipPlacement::Top, TooltipPlacement::Top);
267 assert_eq!(TooltipPlacement::Bottom, TooltipPlacement::Bottom);
268 assert_eq!(TooltipPlacement::Left, TooltipPlacement::Left);
269 assert_eq!(TooltipPlacement::Right, TooltipPlacement::Right);
270 assert_ne!(TooltipPlacement::Top, TooltipPlacement::Bottom);
271 assert_ne!(TooltipPlacement::Top, TooltipPlacement::Left);
272 assert_ne!(TooltipPlacement::Top, TooltipPlacement::Right);
273 assert_ne!(TooltipPlacement::Bottom, TooltipPlacement::Left);
274 assert_ne!(TooltipPlacement::Bottom, TooltipPlacement::Right);
275 assert_ne!(TooltipPlacement::Left, TooltipPlacement::Right);
276 }
277
278 #[test]
279 fn tooltip_placement_clone() {
280 let original = TooltipPlacement::Right;
281 let cloned = original;
282 assert_eq!(original, cloned);
283 }
284
285 #[test]
286 fn tooltip_trigger_default() {
287 assert_eq!(TooltipTrigger::default(), TooltipTrigger::Hover);
288 }
289
290 #[test]
291 fn tooltip_trigger_all_variants() {
292 assert_eq!(TooltipTrigger::Hover, TooltipTrigger::Hover);
293 assert_eq!(TooltipTrigger::Click, TooltipTrigger::Click);
294 assert_ne!(TooltipTrigger::Hover, TooltipTrigger::Click);
295 }
296
297 #[test]
298 fn tooltip_trigger_clone() {
299 let original = TooltipTrigger::Click;
300 let cloned = original;
301 assert_eq!(original, cloned);
302 }
303}