1+ import React , { createContext , useContext , useState , useCallback , ReactNode } from 'react' ;
2+ import { Box } from '@chakra-ui/react' ;
3+
4+ interface TooltipState {
5+ isVisible : boolean ;
6+ content : ReactNode | null ;
7+ position : { x : number ; y : number } ;
8+ anchor : 'top' | 'right' | 'bottom' | 'left' ;
9+ }
10+
11+ interface TooltipContextValue {
12+ showTooltipFromEvent : ( content : ReactNode , event : React . MouseEvent , anchor ?: 'top' | 'right' | 'bottom' | 'left' ) => void ;
13+ hideTooltip : ( ) => void ;
14+ tooltipState : TooltipState ;
15+ }
16+
17+ const TooltipContext = createContext < TooltipContextValue | undefined > ( undefined ) ;
18+
19+ interface TooltipProviderProps {
20+ children : ReactNode ;
21+ }
22+
23+ export const TooltipProvider : React . FC < TooltipProviderProps > = ( { children } ) => {
24+ const [ tooltipState , setTooltipState ] = useState < TooltipState > ( {
25+ isVisible : false ,
26+ content : null ,
27+ position : { x : 0 , y : 0 } ,
28+ anchor : 'top'
29+ } ) ;
30+
31+ const showTooltipFromEvent = useCallback ( (
32+ content : ReactNode ,
33+ event : React . MouseEvent ,
34+ anchor : 'top' | 'right' | 'bottom' | 'left' = 'top'
35+ ) => {
36+ const x = event . clientX ;
37+ const y = event . clientY ;
38+
39+ setTooltipState ( {
40+ isVisible : true ,
41+ content,
42+ position : { x, y } ,
43+ anchor
44+ } ) ;
45+ } , [ ] ) ;
46+
47+ const hideTooltip = useCallback ( ( ) => {
48+ setTooltipState ( prev => ( {
49+ ...prev ,
50+ isVisible : false ,
51+ content : null
52+ } ) ) ;
53+ } , [ ] ) ;
54+
55+ return (
56+ < TooltipContext . Provider value = { { showTooltipFromEvent, hideTooltip, tooltipState } } >
57+ { children }
58+ { tooltipState . isVisible && tooltipState . content && (
59+ < TooltipPortal
60+ content = { tooltipState . content }
61+ position = { tooltipState . position }
62+ anchor = { tooltipState . anchor }
63+ />
64+ ) }
65+ </ TooltipContext . Provider >
66+ ) ;
67+ } ;
68+
69+ interface TooltipPortalProps {
70+ content : ReactNode ;
71+ position : { x : number ; y : number } ;
72+ anchor : 'top' | 'right' | 'bottom' | 'left' ;
73+ }
74+
75+ const TooltipPortal : React . FC < TooltipPortalProps > = ( { content, position, anchor } ) => {
76+ const getTooltipPosition = ( ) => {
77+ const offset = 10 ;
78+ const tooltipWidth = 420 ; // Based on TooltipCard minW="420px"
79+ const tooltipHeight = 300 ; // Estimated height
80+ const viewportWidth = window . innerWidth ;
81+ const viewportHeight = window . innerHeight ;
82+
83+ let finalAnchor = anchor ;
84+ let x = position . x ;
85+ let y = position . y ;
86+
87+ // Adjust anchor and position based on viewport boundaries
88+ switch ( anchor ) {
89+ case 'right' :
90+ // Check if tooltip would go beyond right edge
91+ if ( x + offset + tooltipWidth > viewportWidth ) {
92+ finalAnchor = 'left' ;
93+ }
94+ // Check if tooltip would go beyond bottom edge
95+ if ( y + tooltipHeight / 2 > viewportHeight ) {
96+ y = viewportHeight - tooltipHeight / 2 - offset ;
97+ }
98+ // Check if tooltip would go beyond top edge
99+ if ( y - tooltipHeight / 2 < 0 ) {
100+ y = tooltipHeight / 2 + offset ;
101+ }
102+ break ;
103+
104+ case 'left' :
105+ // Check if tooltip would go beyond left edge
106+ if ( x - offset - tooltipWidth < 0 ) {
107+ finalAnchor = 'right' ;
108+ }
109+ // Check vertical boundaries
110+ if ( y + tooltipHeight / 2 > viewportHeight ) {
111+ y = viewportHeight - tooltipHeight / 2 - offset ;
112+ }
113+ if ( y - tooltipHeight / 2 < 0 ) {
114+ y = tooltipHeight / 2 + offset ;
115+ }
116+ break ;
117+
118+ case 'bottom' :
119+ // Check if tooltip would go beyond bottom edge
120+ if ( y + offset + tooltipHeight > viewportHeight ) {
121+ finalAnchor = 'top' ;
122+ }
123+ // Check horizontal boundaries
124+ if ( x + tooltipWidth / 2 > viewportWidth ) {
125+ x = viewportWidth - tooltipWidth / 2 - offset ;
126+ }
127+ if ( x - tooltipWidth / 2 < 0 ) {
128+ x = tooltipWidth / 2 + offset ;
129+ }
130+ break ;
131+
132+ case 'top' :
133+ default :
134+ // Check if tooltip would go beyond top edge
135+ if ( y - offset - tooltipHeight < 0 ) {
136+ finalAnchor = 'bottom' ;
137+ }
138+ // Check horizontal boundaries
139+ if ( x + tooltipWidth / 2 > viewportWidth ) {
140+ x = viewportWidth - tooltipWidth / 2 - offset ;
141+ }
142+ if ( x - tooltipWidth / 2 < 0 ) {
143+ x = tooltipWidth / 2 + offset ;
144+ }
145+ break ;
146+ }
147+
148+ // Return position based on final anchor
149+ switch ( finalAnchor ) {
150+ case 'right' :
151+ return {
152+ left : x + offset ,
153+ top : y ,
154+ transform : 'translateY(-50%)'
155+ } ;
156+ case 'left' :
157+ return {
158+ left : x - offset - tooltipWidth ,
159+ top : y ,
160+ transform : 'translateY(-50%)'
161+ } ;
162+ case 'bottom' :
163+ return {
164+ left : x ,
165+ top : y + offset ,
166+ transform : 'translateX(-50%)'
167+ } ;
168+ case 'top' :
169+ default :
170+ return {
171+ left : x ,
172+ top : y - offset - tooltipHeight ,
173+ transform : 'translateX(-50%)'
174+ } ;
175+ }
176+ } ;
177+
178+ return (
179+ < Box
180+ position = "fixed"
181+ zIndex = { 9999 }
182+ pointerEvents = "none"
183+ style = { getTooltipPosition ( ) }
184+ >
185+ { content }
186+ </ Box >
187+ ) ;
188+ } ;
189+
190+ export const useCustomTooltip = ( ) : TooltipContextValue => {
191+ const context = useContext ( TooltipContext ) ;
192+ if ( context === undefined ) {
193+ throw new Error ( 'useCustomTooltip must be used within a TooltipProvider' ) ;
194+ }
195+ return context ;
196+ } ;
0 commit comments