tooltip.tsx raw
1 import * as React from "react";
2 import { type PropsWithChildren, createContext, useContext } from "react";
3 import {
4 Popover as PopoverPrimitive,
5 Tooltip as TooltipPrimitive,
6 } from "radix-ui";
7
8 import { cn } from "src/lib/utils";
9
10 import { Popover, PopoverContent, PopoverTrigger } from "./popover";
11
12 function TooltipProvider({
13 delayDuration = 0,
14 ...props
15 }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
16 return (
17 <TooltipPrimitive.Provider
18 data-slot="tooltip-provider"
19 delayDuration={delayDuration}
20 {...props}
21 />
22 );
23 }
24
25 function RadixTooltipRootWithProvider(
26 props: React.ComponentProps<typeof TooltipPrimitive.Root>
27 ) {
28 return (
29 <TooltipProvider delayDuration={200}>
30 <TooltipPrimitive.Root data-slot="tooltip" {...props} />
31 </TooltipProvider>
32 );
33 }
34
35 function BaseTooltipContent({
36 className,
37 sideOffset = 0,
38 children,
39 ...props
40 }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
41 return (
42 <TooltipPrimitive.Portal>
43 <TooltipPrimitive.Content
44 data-slot="tooltip-content"
45 sideOffset={sideOffset}
46 className={cn(
47 "z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-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 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
48 className
49 )}
50 {...props}
51 >
52 {children}
53 <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
54 </TooltipPrimitive.Content>
55 </TooltipPrimitive.Portal>
56 );
57 }
58
59 // Hybrid tooltip for touch devices
60
61 const TouchContext = createContext<boolean | undefined>(undefined);
62
63 const useTouch = () => useContext(TouchContext);
64
65 function TouchProvider(props: PropsWithChildren) {
66 const isTouch = React.useMemo(
67 () => window.matchMedia("(pointer: coarse)").matches,
68 []
69 );
70
71 return <TouchContext.Provider value={isTouch} {...props} />;
72 }
73
74 type HybridTooltipProps = React.ComponentProps<typeof TooltipPrimitive.Root> &
75 React.ComponentProps<typeof PopoverPrimitive.Root>;
76
77 function HybridTooltip(props: HybridTooltipProps) {
78 const isTouch = useTouch();
79
80 return isTouch ? (
81 <Popover {...props} />
82 ) : (
83 <RadixTooltipRootWithProvider {...props} />
84 );
85 }
86
87 type HybridTooltipTriggerProps = React.ComponentProps<
88 typeof TooltipPrimitive.Trigger
89 > &
90 React.ComponentProps<typeof PopoverPrimitive.Trigger>;
91
92 function HybridTooltipTrigger(props: HybridTooltipTriggerProps) {
93 const isTouch = useTouch();
94
95 return isTouch ? (
96 <PopoverTrigger {...props} />
97 ) : (
98 <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
99 );
100 }
101
102 type HybridTooltipContentProps = React.ComponentProps<
103 typeof TooltipPrimitive.Content
104 > &
105 React.ComponentProps<typeof PopoverPrimitive.Content>;
106
107 function HybridTooltipContent({
108 className,
109 ...props
110 }: HybridTooltipContentProps) {
111 const isTouch = useTouch();
112
113 return isTouch ? (
114 <PopoverContent
115 {...props}
116 className={cn("bg-foreground text-background text-sm", className)}
117 />
118 ) : (
119 <BaseTooltipContent {...props} className={className} />
120 );
121 }
122
123 export {
124 HybridTooltip as Tooltip,
125 HybridTooltipContent as TooltipContent,
126 TooltipProvider,
127 HybridTooltipTrigger as TooltipTrigger,
128 TouchProvider,
129 };
130