PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.15
Yatra – Travel Booking & Tour Operator Software v3.0.15
3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 All 83 releases
yatra / resources / js / components / ui / tooltip.tsx

tooltip.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.15, at resources/js/components/ui/tooltip.tsx

110 lines 4.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Tooltip — small CSS-only hover popover.
3 *
4 * Used by KPI cards on Dashboard / Reports for the `?` info icon
5 * that explains confusable metrics (Booked vs Collected Revenue,
6 * Conversion Rate, Occupancy Rate, etc.).
7 *
8 * Why not the native HTML `title` attribute: native title has a
9 * ~700ms delay, paints in OS-default style (gray, small, easy to
10 * miss), and isn't focusable — keyboard users never see it. This
11 * component shows instantly on pointer-enter AND on keyboard focus,
12 * is themed to match the rest of the admin UI, and uses
13 * `role="tooltip"` + `aria-describedby` so screen readers announce it.
14 *
15 * Implementation is intentionally dependency-free (no Radix /
16 * @floating-ui) — we don't need collision detection / portals for a
17 * help-icon hint that lives next to a known anchor. The trigger is
18 * positioned `relative`, the popover absolutely, and we use
19 * `pointer-events: none` so the tooltip never blocks a click on
20 * something underneath.
21 */
22
23 import React, { useId, useState } from "react";
24
25 interface TooltipProps {
26 /** The text shown in the popover. Pass null/empty to hide entirely. */
27 content: React.ReactNode;
28 /** The anchor — usually the `?` icon. Wrapped in a focusable span. */
29 children: React.ReactNode;
30 /** Where the popover appears relative to the anchor. Default: "top". */
31 side?: "top" | "bottom" | "left" | "right";
32 /** Optional className applied to the anchor wrapper. */
33 className?: string;
34 }
35
36 export const Tooltip: React.FC<TooltipProps> = ({
37 content,
38 children,
39 side = "top",
40 className = "",
41 }) => {
42 const id = useId();
43 const [open, setOpen] = useState(false);
44
45 if (!content) {
46 return <span className={className}>{children}</span>;
47 }
48
49 // Side-specific position classes. The popover is anchored to one
50 // edge of the trigger; a 6px arrow sits on the opposite edge
51 // (computed from `side`). We keep arrow + popover in the same
52 // border + bg color so they read as one unit.
53 const popoverPos: Record<NonNullable<TooltipProps["side"]>, string> = {
54 top: "bottom-full left-1/2 -translate-x-1/2 mb-1.5",
55 bottom: "top-full left-1/2 -translate-x-1/2 mt-1.5",
56 left: "right-full top-1/2 -translate-y-1/2 mr-1.5",
57 right: "left-full top-1/2 -translate-y-1/2 ml-1.5",
58 };
59 const arrowPos: Record<NonNullable<TooltipProps["side"]>, string> = {
60 top: "top-full left-1/2 -translate-x-1/2 border-t-gray-900 dark:border-t-gray-100",
61 bottom:
62 "bottom-full left-1/2 -translate-x-1/2 border-b-gray-900 dark:border-b-gray-100",
63 left: "left-full top-1/2 -translate-y-1/2 border-l-gray-900 dark:border-l-gray-100",
64 right:
65 "right-full top-1/2 -translate-y-1/2 border-r-gray-900 dark:border-r-gray-100",
66 };
67 const arrowStyle: Record<NonNullable<TooltipProps["side"]>, string> = {
68 top: "border-x-transparent border-b-transparent",
69 bottom: "border-x-transparent border-t-transparent",
70 left: "border-y-transparent border-r-transparent",
71 right: "border-y-transparent border-l-transparent",
72 };
73
74 return (
75 <span
76 className={`relative inline-flex items-center ${className}`}
77 onMouseEnter={() => setOpen(true)}
78 onMouseLeave={() => setOpen(false)}
79 onFocus={() => setOpen(true)}
80 onBlur={() => setOpen(false)}
81 onKeyDown={(e) => {
82 if (e.key === "Escape") setOpen(false);
83 }}
84 >
85 {/* Trigger wrapper. We make the inner button tabbable for keyboard
86 access; sighted users still click/hover the same target. */}
87 <span aria-describedby={open ? id : undefined} tabIndex={0}>
88 {children}
89 </span>
90
91 {open && (
92 <span
93 id={id}
94 role="tooltip"
95 className={`absolute z-50 pointer-events-none whitespace-normal break-words rounded-md bg-gray-900 px-2.5 py-1.5 text-xs font-normal leading-snug text-white shadow-lg max-w-xs w-max dark:bg-gray-100 dark:text-gray-900 ${popoverPos[side]}`}
96 >
97 {content}
98 {/* Arrow */}
99 <span
100 aria-hidden="true"
101 className={`absolute h-0 w-0 border-[6px] ${arrowStyle[side]} ${arrowPos[side]}`}
102 />
103 </span>
104 )}
105 </span>
106 );
107 };
108
109 export default Tooltip;
110