radix_leptos_primitives/components/
tooltip.rs1use leptos::callback::Callback;
2use leptos::children::Children;
3use leptos::prelude::*;
4use crate::utils::{merge_optional_classes, generate_id};
5
6#[derive(Debug, Clone, Copy, PartialEq)]
54pub enum TooltipVariant {
55 Default,
56 Destructive,
57 Warning,
58 Info,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq)]
62pub enum TooltipSize {
63 Default,
64 Sm,
65 Lg,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq)]
69pub enum TooltipPosition {
70 Top,
71 Bottom,
72 Left,
73 Right,
74}
75
76impl TooltipVariant {
77 pub fn as_str(&self) -> &'static str {
78 match self {
79 TooltipVariant::Default => "default",
80 TooltipVariant::Destructive => "destructive",
81 TooltipVariant::Warning => "warning",
82 TooltipVariant::Info => "info",
83 }
84 }
85}
86
87impl TooltipSize {
88 pub fn as_str(&self) -> &'static str {
89 match self {
90 TooltipSize::Default => "default",
91 TooltipSize::Sm => "sm",
92 TooltipSize::Lg => "lg",
93 }
94 }
95}
96
97impl TooltipPosition {
98 pub fn as_str(&self) -> &'static str {
99 match self {
100 TooltipPosition::Top => "top",
101 TooltipPosition::Bottom => "bottom",
102 TooltipPosition::Left => "left",
103 TooltipPosition::Right => "right",
104 }
105 }
106}
107
108
109#[component]
111pub fn Tooltip(
112 #[prop(optional, default = false)]
114 open: bool,
115 #[prop(optional, default = false)]
117 disabled: bool,
118 #[prop(optional, default = TooltipVariant::Default)]
120 variant: TooltipVariant,
121 #[prop(optional, default = TooltipSize::Default)]
123 size: TooltipSize,
124 #[prop(optional, default = TooltipPosition::Top)]
126 position: TooltipPosition,
127 #[prop(optional, default = 500)]
129 delay: u32,
130 #[prop(optional, default = 300)]
132 duration: u32,
133 #[prop(optional)]
135 class: Option<String>,
136 #[prop(optional)]
138 style: Option<String>,
139 #[prop(optional)]
141 onopen_change: Option<Callback<bool>>,
142 children: Children,
144) -> impl IntoView {
145 let __tooltip_id = generate_id("tooltip");
146 let trigger_id = generate_id("tooltip-trigger");
147 let content_id = generate_id("tooltip-content");
148
149 let data_variant = variant.as_str();
151 let data_size = size.as_str();
152 let data_position = position.as_str();
153
154 let base_classes = "radix-tooltip";
156 let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
157 .unwrap_or_else(|| base_classes.to_string());
158
159 let handle_keydown = move |e: web_sys::KeyboardEvent| match e.key().as_str() {
161 "Enter" | " " => {
162 e.prevent_default();
163 if !disabled {
164 if let Some(onopen_change) = onopen_change {
165 onopen_change.run(!open);
166 }
167 }
168 }
169 "Escape" => {
170 e.prevent_default();
171 if let Some(onopen_change) = onopen_change {
172 onopen_change.run(false);
173 }
174 }
175 _ => {}
176 };
177
178 view! {
179 <div
180 class=combined_class
181 style=style
182 data-variant=data_variant
183 data-size=data_size
184 data-position=data_position
185 data-open=open
186 data-disabled=disabled
187 data-delay=delay
188 data-duration=duration
189 on:keydown=handle_keydown
190 >
191 {children()}
192 </div>
193 }
194}
195
196#[component]
198pub fn TooltipTrigger(
199 #[prop(optional)]
201 class: Option<String>,
202 #[prop(optional)]
204 style: Option<String>,
205 children: Children,
207) -> impl IntoView {
208 let trigger_id = generate_id("tooltip-trigger");
209
210 let base_classes = "radix-tooltip-trigger";
211 let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
212 .unwrap_or_else(|| base_classes.to_string());
213
214 let handle_mouse_enter = move |e: web_sys::MouseEvent| {
216 e.prevent_default();
217 };
219
220 let handle_mouse_leave = move |e: web_sys::MouseEvent| {
221 e.prevent_default();
222 };
224
225 let handle_focus = move |e: web_sys::FocusEvent| {
227 e.prevent_default();
228 };
230
231 let handle_blur = move |e: web_sys::FocusEvent| {
232 e.prevent_default();
233 };
235
236 view! {
237 <div
238 class=combined_class
239 style=style
240 id=trigger_id
241 aria-describedby="tooltip-content"
242 on:mouseenter=handle_mouse_enter
243 on:mouseleave=handle_mouse_leave
244 on:focus=handle_focus
245 on:blur=handle_blur
246 >
247 {children()}
248 </div>
249 }
250}
251
252#[component]
254pub fn TooltipContent(
255 #[prop(optional)]
257 class: Option<String>,
258 #[prop(optional)]
260 style: Option<String>,
261 children: Children,
263) -> impl IntoView {
264 let content_id = generate_id("tooltip-content");
265
266 let base_classes = "radix-tooltip-content";
267 let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
268 .unwrap_or_else(|| base_classes.to_string());
269
270 view! {
271 <div
272 class=combined_class
273 style=style
274 id=content_id
275 role="tooltip"
276 data-state="closed"
277 >
278 {children()}
279 </div>
280 }
281}
282
283#[component]
285pub fn TooltipArrow(
286 #[prop(optional)]
288 class: Option<String>,
289 #[prop(optional)]
291 style: Option<String>,
292) -> impl IntoView {
293 let base_classes = "radix-tooltip-arrow";
294 let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
295 .unwrap_or_else(|| base_classes.to_string());
296
297 view! {
298 <div
299 class=combined_class
300 style=style
301 data-popper-arrow=""
302 >
303 </div>
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use crate::{TooltipPosition, TooltipSize, TooltipVariant};
310 use proptest::prelude::*;
311
312 #[test]
314 fn test_tooltip_variants() {
315 run_test(|| {
316 let variants = [
318 TooltipVariant::Default,
319 TooltipVariant::Destructive,
320 TooltipVariant::Warning,
321 TooltipVariant::Info,
322 ];
323
324 for variant in variants {
325 assert!(!variant.as_str().is_empty());
327 }
328 });
329 }
330
331 #[test]
332 fn test_tooltip_sizes() {
333 run_test(|| {
334 let sizes = [TooltipSize::Default, TooltipSize::Sm, TooltipSize::Lg];
335
336 for size in sizes {
337 assert!(!size.as_str().is_empty());
339 }
340 });
341 }
342
343 #[test]
344 fn test_tooltip_positions() {
345 run_test(|| {
346 let positions = [
347 TooltipPosition::Top,
348 TooltipPosition::Bottom,
349 TooltipPosition::Left,
350 TooltipPosition::Right,
351 ];
352
353 for position in positions {
354 assert!(!position.as_str().is_empty());
356 }
357 });
358 }
359
360 #[test]
362 fn test_tooltipopen_state() {
363 run_test(|| {
364 let open = true;
366 let disabled = false;
367 let variant = TooltipVariant::Default;
368 let size = TooltipSize::Default;
369 let position = TooltipPosition::Top;
370 let delay = 500;
371 let duration = 300;
372
373 assert!(open);
375 assert!(!disabled);
376 assert_eq!(variant, TooltipVariant::Default);
377 assert_eq!(size, TooltipSize::Default);
378 assert_eq!(position, TooltipPosition::Top);
379 assert_eq!(delay, 500);
380 assert_eq!(duration, 300);
381 });
382 }
383
384 #[test]
385 fn test_tooltip_closed_state() {
386 run_test(|| {
387 let open = false;
389 let disabled = true;
390 let variant = TooltipVariant::Destructive;
391 let size = TooltipSize::Lg;
392 let position = TooltipPosition::Bottom;
393 let delay = 1000;
394 let duration = 500;
395
396 assert!(!open);
398 assert!(disabled);
399 assert_eq!(variant, TooltipVariant::Destructive);
400 assert_eq!(size, TooltipSize::Lg);
401 assert_eq!(position, TooltipPosition::Bottom);
402 assert_eq!(delay, 1000);
403 assert_eq!(duration, 500);
404 });
405 }
406
407 #[test]
409 fn test_tooltip_state_changes() {
410 run_test(|| {
411 let mut open = false;
413 let disabled = false;
414 let delay = 500;
415 let duration = 300;
416
417 assert!(!open);
419 assert!(!disabled);
420 assert_eq!(delay, 500);
421 assert_eq!(duration, 300);
422
423 open = true;
425
426 assert!(open);
427 assert!(!disabled);
428 assert_eq!(delay, 500);
429 assert_eq!(duration, 300);
430
431 open = false;
433
434 assert!(!open);
435 assert!(!disabled);
436 assert_eq!(delay, 500);
437 assert_eq!(duration, 300);
438 });
439 }
440
441 #[test]
443 fn test_tooltip_keyboard_navigation() {
444 run_test(|| {
445 let enter_pressed = true;
447 let space_pressed = false;
448 let escape_pressed = false;
449 let disabled = false;
450
451 assert!(enter_pressed);
453 assert!(!space_pressed);
454 assert!(!escape_pressed);
455 assert!(!disabled);
456
457 if enter_pressed && !disabled {
459 }
461
462 if space_pressed && !disabled {
464 panic!("Unexpected condition reached"); }
467
468 if escape_pressed {
470 panic!("Unexpected condition reached"); }
473 });
474 }
475
476 #[test]
477 fn test_tooltip_mouse_events() {
478 run_test(|| {
479 let mouse_enter = true;
481 let mouse_leave = false;
482 let disabled = false;
483 let delay = 500;
484
485 assert!(mouse_enter);
487 assert!(!mouse_leave);
488 assert!(!disabled);
489 assert_eq!(delay, 500);
490
491 if mouse_enter && !disabled {
493 }
495
496 if mouse_leave && !disabled {
498 panic!("Unexpected condition reached"); }
501 });
502use crate::utils::{merge_optional_classes, generate_id};
503 }
504
505 #[test]
506 fn test_tooltip_focus_events() {
507 run_test(|| {
508 let focus = true;
510 let blur = false;
511 let disabled = false;
512
513 assert!(focus);
515 assert!(!blur);
516 assert!(!disabled);
517
518 if focus && !disabled {
520 }
522
523 if blur && !disabled {
525 panic!("Unexpected condition reached"); }
528 });
529 }
530
531 #[test]
533 fn test_tooltip_accessibility() {
534 run_test(|| {
535 let role = "tooltip";
537 let aria_describedby = "tooltip-content";
538 let data_state = "closed";
539
540 assert_eq!(role, "tooltip");
542 assert_eq!(aria_describedby, "tooltip-content");
543 assert_eq!(data_state, "closed");
544 });
545 }
546
547 #[test]
549 fn test_tooltip_edge_cases() {
550 run_test(|| {
551 let open = false;
553 let delay = 0;
554 let duration = 0;
555 let disabled = false;
556
557 assert!(!open);
559 assert_eq!(delay, 0);
560 assert_eq!(duration, 0);
561 assert!(!disabled);
562 });
563 }
564
565 #[test]
566 fn test_tooltipdisabled_state() {
567 run_test(|| {
568 let disabled = true;
570 let open = false;
571 let delay = 500;
572 let duration = 300;
573
574 assert!(disabled);
576 assert!(!open);
577 assert_eq!(delay, 500);
578 assert_eq!(duration, 300);
579
580 });
582 }
583
584 #[test]
585 fn test_tooltip_positioning() {
586 run_test(|| {
587 let position = TooltipPosition::Top;
589 let open = true;
590 let disabled = false;
591
592 assert_eq!(position, TooltipPosition::Top);
594 assert!(open);
595 assert!(!disabled);
596
597 let positions = [
599 TooltipPosition::Bottom,
600 TooltipPosition::Left,
601 TooltipPosition::Right,
602 ];
603
604 for pos in positions {
605 assert!(!pos.as_str().is_empty());
606 }
607 });
608 }
609
610 proptest! {
612 #[test]
613 fn test_tooltip_properties(
614 variant in prop::sample::select(&[
615 TooltipVariant::Default,
616 TooltipVariant::Destructive,
617 TooltipVariant::Warning,
618 TooltipVariant::Info,
619 ]),
620 size in prop::sample::select(&[
621 TooltipSize::Default,
622 TooltipSize::Sm,
623 TooltipSize::Lg,
624 ]),
625 position in prop::sample::select(&[
626 TooltipPosition::Top,
627 TooltipPosition::Bottom,
628 TooltipPosition::Left,
629 TooltipPosition::Right,
630 ]),
631 open in prop::bool::ANY,
632 disabled in prop::bool::ANY,
633 __delay in 0..2000u32,
634 __duration in 0..2000u32
635 ) {
636 assert!(!variant.as_str().is_empty());
639 assert!(!size.as_str().is_empty());
640 assert!(!position.as_str().is_empty());
641
642 assert!(matches!(open, true | false));
644 assert!(matches!(disabled, true | false));
645
646 assert!(__delay <= 2000);
648 assert!(__duration <= 2000);
649
650 if disabled {
652 }
655
656 if __delay > 1000 {
658 }
661 }
662 }
663
664 fn run_test<F>(f: F)
666 where
667 F: FnOnce(),
668 {
669 f();
671 }
672}