1- import { AnimatePresence , domAnimation , LazyMotion , m } from 'framer-motion'
2- import { type ReactNode , useEffect , useRef , useState } from 'react'
3- import ReactDOM from 'react-dom'
1+ import type { ReactNode } from 'react'
42
53type Position =
64 | 'top'
@@ -16,233 +14,13 @@ interface TooltipProps {
1614 children : ReactNode
1715 content : ReactNode | null
1816 position ?: Position
19- offset ?: number
20- disableAutoPosition ?: boolean
21- className ?: string
22- contentClassName ?: string
23- delay ?: number
24- alwaysShow ?: boolean
2517}
2618
27- const Tooltip = ( {
28- children,
29- content,
30- position = 'top' ,
31- offset = 10 ,
32- disableAutoPosition = false ,
33- className = '' ,
34- contentClassName = '' ,
35- delay = 0 ,
36- alwaysShow = false ,
37- } : TooltipProps ) => {
38- const [ isVisible , setIsVisible ] = useState ( false )
39- const [ calculatedPosition , setCalculatedPosition ] = useState < Position > ( position )
40- const [ tooltipCoords , setTooltipCoords ] = useState ( { x : 0 , y : 0 } )
41- const [ delayTimeout , setDelayTimeout ] = useState < NodeJS . Timeout | null > ( null )
42-
43- const triggerRef = useRef < HTMLDivElement > ( null )
44- const tooltipRef = useRef < HTMLDivElement > ( null )
45-
46- const calculatePosition = ( ) => {
47- if ( ! triggerRef . current || ! tooltipRef . current ) return
48-
49- const triggerRect = triggerRef . current . getBoundingClientRect ( )
50- const tooltipRect = tooltipRef . current . getBoundingClientRect ( )
51- const viewportWidth = window . innerWidth
52- const viewportHeight = window . innerHeight
53-
54- let newPosition = position
55- let x = 0
56- let y = 0
57-
58- switch ( position ) {
59- case 'top' :
60- x = triggerRect . left + triggerRect . width / 2 - tooltipRect . width / 2
61- y = triggerRect . top - tooltipRect . height - offset
62- break
63- case 'right' :
64- x = triggerRect . right + offset
65- y = triggerRect . top + triggerRect . height / 2 - tooltipRect . height / 2
66- break
67- case 'bottom' :
68- x = triggerRect . left + triggerRect . width / 2 - tooltipRect . width / 2
69- y = triggerRect . bottom + offset
70- break
71- case 'left' :
72- x = triggerRect . left - tooltipRect . width - offset
73- y = triggerRect . top + triggerRect . height / 2 - tooltipRect . height / 2
74- break
75- case 'bottom-right' :
76- x = triggerRect . right
77- y = triggerRect . bottom + offset
78- break
79- case 'bottom-left' :
80- x = triggerRect . left - tooltipRect . width
81- y = triggerRect . bottom + offset
82- break
83- case 'top-right' :
84- x = triggerRect . right
85- y = triggerRect . top - tooltipRect . height - offset
86- break
87- case 'top-left' :
88- x = triggerRect . left - tooltipRect . width
89- y = triggerRect . top - tooltipRect . height - offset
90- break
91- }
92-
93- if ( ! disableAutoPosition ) {
94- if ( position === 'top' && y < 0 ) {
95- y = triggerRect . bottom + offset
96- newPosition = 'bottom'
97- } else if ( position === 'bottom' && y + tooltipRect . height > viewportHeight ) {
98- y = triggerRect . top - tooltipRect . height - offset
99- newPosition = 'top'
100- } else if ( position === 'left' && x < 0 ) {
101- x = triggerRect . right + offset
102- newPosition = 'right'
103- } else if ( position === 'right' && x + tooltipRect . width > viewportWidth ) {
104- x = triggerRect . left - tooltipRect . width - offset
105- newPosition = 'left'
106- }
107- }
108-
109- x = Math . max ( 10 , Math . min ( x , viewportWidth - tooltipRect . width - 10 ) )
110- y = Math . max ( 10 , Math . min ( y , viewportHeight - tooltipRect . height - 10 ) )
111-
112- setCalculatedPosition ( newPosition )
113- setTooltipCoords ( { x, y } )
114- }
115-
116- const showTooltip = ( ) => {
117- if ( delay && delay > 0 ) {
118- const timeout = setTimeout ( ( ) => {
119- setIsVisible ( true )
120- } , delay )
121- setDelayTimeout ( timeout )
122- } else {
123- setIsVisible ( true )
124- }
125- }
126-
127- const hideTooltip = ( ) => {
128- if ( delayTimeout ) {
129- clearTimeout ( delayTimeout )
130- setDelayTimeout ( null )
131- }
132- setIsVisible ( false )
133- }
134-
135- useEffect ( ( ) => {
136- if ( alwaysShow || isVisible ) {
137- calculatePosition ( )
138-
139- const handleResize = ( ) => calculatePosition ( )
140- window . addEventListener ( 'resize' , handleResize )
141- window . addEventListener ( 'scroll' , handleResize , true )
142-
143- return ( ) => {
144- window . removeEventListener ( 'resize' , handleResize )
145- window . removeEventListener ( 'scroll' , handleResize , true )
146- }
147- }
148- } , [ alwaysShow , isVisible ] )
149-
150- useEffect ( ( ) => {
151- return ( ) => {
152- if ( delayTimeout ) {
153- clearTimeout ( delayTimeout )
154- }
155- }
156- } , [ delayTimeout ] )
157-
158- // Refined animations
159- const variants = {
160- top : {
161- hidden : { opacity : 0 , y : 2 } ,
162- visible : { opacity : 1 , y : 0 } ,
163- } ,
164- right : {
165- hidden : { opacity : 0 , x : - 2 } ,
166- visible : { opacity : 1 , x : 0 } ,
167- } ,
168- bottom : {
169- hidden : { opacity : 0 , y : - 2 } ,
170- visible : { opacity : 1 , y : 0 } ,
171- } ,
172- left : {
173- hidden : { opacity : 0 , x : 2 } ,
174- visible : { opacity : 1 , x : 0 } ,
175- } ,
176- // For corner positions, use the closest main direction for animation
177- 'bottom-right' : { hidden : { opacity : 0 , y : - 2 } , visible : { opacity : 1 , y : 0 } } ,
178- 'bottom-left' : { hidden : { opacity : 0 , y : - 2 } , visible : { opacity : 1 , y : 0 } } ,
179- 'top-right' : { hidden : { opacity : 0 , y : 2 } , visible : { opacity : 1 , y : 0 } } ,
180- 'top-left' : { hidden : { opacity : 0 , y : 2 } , visible : { opacity : 1 , y : 0 } } ,
181- }
182-
183- if ( ! content ) {
184- return < > { children } </ >
185- }
186-
187- // Helper to get arrow classes based on calculated position
188- const getArrowClasses = ( pos : Position ) => {
189- const base = 'absolute w-0 h-0 border-solid'
190- switch ( pos ) {
191- case 'top' :
192- case 'top-left' :
193- case 'top-right' :
194- return `${ base } bottom-[-5px] left-1/2 -translate-x-1/2 border-l-[6px] border-r-[6px] border-t-[6px] border-l-transparent border-r-transparent border-t-base-200`
195- case 'bottom' :
196- case 'bottom-left' :
197- case 'bottom-right' :
198- return `${ base } top-[-5px] left-1/2 -translate-x-1/2 border-l-[6px] border-r-[6px] border-b-[6px] border-l-transparent border-r-transparent border-b-base-200`
199- case 'left' :
200- return `${ base } right-[-5px] top-1/2 -translate-y-1/2 border-t-[6px] border-b-[6px] border-l-[6px] border-t-transparent border-b-transparent border-l-base-200`
201- case 'right' :
202- return `${ base } left-[-5px] top-1/2 -translate-y-1/2 border-t-[6px] border-b-[6px] border-r-[6px] border-t-transparent border-b-transparent border-r-base-200`
203- }
204- }
205-
19+ const Tooltip = ( { children, content } : TooltipProps ) => {
20620 return (
207- < >
208- < div
209- ref = { triggerRef }
210- className = { `inline-block ${ className } ` }
211- onMouseEnter = { showTooltip }
212- onMouseLeave = { hideTooltip }
213- onFocus = { showTooltip }
214- onBlur = { hideTooltip }
215- >
216- { children }
217- </ div >
218-
219- { ( alwaysShow || isVisible ) &&
220- ReactDOM . createPortal (
221- < LazyMotion features = { domAnimation } >
222- < AnimatePresence >
223- < m . div
224- ref = { tooltipRef }
225- className = { `tooltip fixed rounded-lg py-1.5 px-3 text-xs max-w-xs bg-base-200 shadow-lg z-[9999] ${ contentClassName } ` }
226- style = { {
227- left : tooltipCoords . x ,
228- top : tooltipCoords . y ,
229- } }
230- initial = "hidden"
231- animate = "visible"
232- exit = "hidden"
233- variants = { variants [ calculatedPosition ] }
234- transition = { { duration : 0.15 , ease : 'easeOut' } }
235- onMouseEnter = { showTooltip }
236- onMouseLeave = { hideTooltip }
237- >
238- { content }
239- < div className = { getArrowClasses ( calculatedPosition ) } />
240- </ m . div >
241- </ AnimatePresence >
242- </ LazyMotion > ,
243- document . body
244- ) }
245- </ >
21+ < div className = "tooltip tooltip-left" data-tip = { content } >
22+ { children }
23+ </ div >
24624 )
24725}
24826
0 commit comments