1+ import React , { useState , useRef , useEffect } from 'react' ;
2+ import { createPortal } from 'react-dom' ;
3+ import { cn } from '@/lib/utils' ;
4+
5+ // Context for transferring state between components
6+ const TooltipContext = React . createContext < {
7+ open : boolean ;
8+ setOpen : ( open : boolean ) => void ;
9+ content : React . ReactNode ;
10+ setContent : ( content : React . ReactNode ) => void ;
11+ triggerRef : React . RefObject < HTMLDivElement > ;
12+ } > ( {
13+ open : false ,
14+ setOpen : ( ) => { } ,
15+ content : null ,
16+ setContent : ( ) => { } ,
17+ triggerRef : { current : null } ,
18+ } ) ;
19+
20+ // Provider component
21+ export const TooltipProvider : React . FC < { children : React . ReactNode } > = ( { children } ) => {
22+ return < > { children } </ > ;
23+ } ;
24+
25+ // Main tooltip component
26+ export const Tooltip : React . FC < { children : React . ReactNode } > = ( { children } ) => {
27+ const [ open , setOpen ] = useState ( false ) ;
28+ const [ content , setContent ] = useState < React . ReactNode > ( null ) ;
29+ const triggerRef = useRef < HTMLDivElement > ( null ) ;
30+
31+ // Parse children to find TooltipTrigger and TooltipContent
32+ let triggerElement : React . ReactElement | null = null ;
33+ let contentElement : React . ReactElement | null = null ;
34+
35+ React . Children . forEach ( children , ( child ) => {
36+ if ( ! React . isValidElement ( child ) ) return ;
37+
38+ if ( child . type === TooltipTrigger ) {
39+ triggerElement = child ;
40+ } else if ( child . type === TooltipContent ) {
41+ contentElement = child ;
42+ }
43+ } ) ;
44+
45+ // Set content from TooltipContent element
46+ useEffect ( ( ) => {
47+ if ( contentElement && contentElement . props . children ) {
48+ setContent ( contentElement . props . children ) ;
49+ }
50+ } , [ contentElement ] ) ;
51+
52+ return (
53+ < TooltipContext . Provider value = { { open, setOpen, content, setContent, triggerRef } } >
54+ { triggerElement }
55+ { open && content && < TooltipPortal /> }
56+ </ TooltipContext . Provider >
57+ ) ;
58+ } ;
59+
60+ // Trigger component
61+ export const TooltipTrigger : React . FC < {
62+ children : React . ReactNode ;
63+ asChild ?: boolean ;
64+ } > = ( { children } ) => {
65+ const { setOpen, triggerRef } = React . useContext ( TooltipContext ) ;
66+
67+ return (
68+ < div
69+ ref = { triggerRef }
70+ onMouseEnter = { ( ) => setOpen ( true ) }
71+ onMouseLeave = { ( ) => setOpen ( false ) }
72+ onFocus = { ( ) => setOpen ( true ) }
73+ onBlur = { ( ) => setOpen ( false ) }
74+ className = "inline-block"
75+ >
76+ { children }
77+ </ div >
78+ ) ;
79+ } ;
80+
81+ // Content component - just stores content in context
82+ export const TooltipContent : React . FC < {
83+ children : React . ReactNode ;
84+ className ?: string ;
85+ sideOffset ?: number ;
86+ } > = (
87+ // We're intentionally not using the props here but TooltipContent element acts as a data container
88+ { children : _children }
89+ ) => {
90+ return null ; // This doesn't render directly, content is passed to portal via context
91+ } ;
92+
93+ // Portal component that actually renders the tooltip
94+ const TooltipPortal : React . FC = ( ) => {
95+ const { open, content, triggerRef } = React . useContext ( TooltipContext ) ;
96+ const tooltipRef = useRef < HTMLDivElement > ( null ) ;
97+ const [ position , setPosition ] = useState ( { top : 0 , left : 0 } ) ;
98+
99+ // Calculate position based on trigger
100+ useEffect ( ( ) => {
101+ if ( ! triggerRef . current || ! tooltipRef . current ) return ;
102+
103+ const updatePosition = ( ) => {
104+ const triggerRect = triggerRef . current ! . getBoundingClientRect ( ) ;
105+ const tooltipRect = tooltipRef . current ! . getBoundingClientRect ( ) ;
106+
107+ // Default positioning (above the element)
108+ const top = triggerRect . top - tooltipRect . height - 5 ;
109+ const left = triggerRect . left + ( triggerRect . width - tooltipRect . width ) / 2 ;
110+
111+ // Adjust for scroll position
112+ setPosition ( {
113+ top : top + window . scrollY ,
114+ left : left + window . scrollX
115+ } ) ;
116+ } ;
117+
118+ updatePosition ( ) ;
119+ window . addEventListener ( 'resize' , updatePosition ) ;
120+ window . addEventListener ( 'scroll' , updatePosition ) ;
121+
122+ return ( ) => {
123+ window . removeEventListener ( 'resize' , updatePosition ) ;
124+ window . removeEventListener ( 'scroll' , updatePosition ) ;
125+ } ;
126+ } , [ open , triggerRef . current , tooltipRef . current ] ) ;
127+
128+ if ( ! open ) return null ;
129+
130+ return createPortal (
131+ < div
132+ ref = { tooltipRef }
133+ className = { cn (
134+ 'z-50 overflow-hidden rounded-md border bg-white px-3 py-1.5 text-sm shadow-md' ,
135+ 'absolute'
136+ ) }
137+ style = { {
138+ top : `${ position . top } px` ,
139+ left : `${ position . left } px` ,
140+ zIndex : 9999 ,
141+ } }
142+ >
143+ { content }
144+ </ div > ,
145+ document . body
146+ ) ;
147+ } ;
0 commit comments