Skip to content

Commit b50cf67

Browse files
committed
improve tooltip
1 parent fcc73a1 commit b50cf67

1 file changed

Lines changed: 5 additions & 227 deletions

File tree

src/renderer/component/tooltip/toolTip.tsx

Lines changed: 5 additions & 227 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
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

53
type 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

Comments
 (0)