Enhance CursorTooltip with Visibility Control and CSS Transitions

- Updated the CursorTooltip component to accept a new `visible` prop, allowing for better control over tooltip visibility.
- Refactored tooltip rendering logic to utilize CSS transitions for smoother fade-in and fade-out effects, improving user experience.
- Enhanced TooltipContext to manage tooltip visibility state and timing, ensuring consistent behavior during interactions.
- Updated CSS styles for the cursor tooltip to support new visibility transitions, enhancing visual feedback.
This commit is contained in:
Tom Butcher 2026-08-20 21:53:52 +01:00
parent ef57837371
commit 80ac635bf9
3 changed files with 80 additions and 36 deletions

View File

@ -361,6 +361,16 @@ code {
padding: 8px !important;
}
.cursor-tooltip {
opacity: 0;
transition: opacity 0.5s ease;
}
.cursor-tooltip.is-visible {
opacity: 1;
transition: opacity 0.15s ease;
}
.cursor-tooltip .ant-card {
box-shadow:
0 6px 16px 0 rgba(0, 0, 0, 0.08),

View File

@ -15,7 +15,7 @@ const getTooltipRoot = () => {
return root
}
const CursorTooltip = ({ content, pointRef }) => {
const CursorTooltip = ({ content, visible, pointRef }) => {
const tooltipRef = useRef(null)
const sizeRef = useRef({ width: 0, height: 0 })
const frameRef = useRef(null)
@ -94,23 +94,19 @@ const CursorTooltip = ({ content, pointRef }) => {
}
}, [applyPosition, root, schedulePosition])
const visible = content != null && content !== false && content !== ''
if (!root) return null
return createPortal(
<div
ref={tooltipRef}
className='cursor-tooltip'
className={`cursor-tooltip${visible ? ' is-visible' : ''}`}
style={{
position: 'fixed',
top: 0,
left: 0,
zIndex: 1100,
pointerEvents: 'none',
visibility: visible ? 'visible' : 'hidden',
opacity: visible ? 1 : 0,
willChange: 'transform'
willChange: 'transform, opacity'
}}
>
<Card
@ -131,6 +127,7 @@ const CursorTooltip = ({ content, pointRef }) => {
CursorTooltip.propTypes = {
content: PropTypes.node,
visible: PropTypes.bool,
pointRef: PropTypes.shape({
current: PropTypes.shape({
x: PropTypes.number,

View File

@ -11,11 +11,31 @@ import CursorTooltip from '../common/CursorTooltip'
const TooltipContext = createContext(null)
const HIDE_DELAY_MS = 100
const FADE_OUT_MS = 500
export const TooltipProvider = ({ children }) => {
const [content, setContent] = useState(null)
const [visible, setVisible] = useState(false)
const stackRef = useRef([])
const hideTimerRef = useRef(null)
const fadeTimerRef = useRef(null)
const pointRef = useRef({ x: 0, y: 0 })
const visibleRef = useRef(false)
visibleRef.current = visible
const clearTimers = useCallback(() => {
if (hideTimerRef.current != null) {
window.clearTimeout(hideTimerRef.current)
hideTimerRef.current = null
}
if (fadeTimerRef.current != null) {
window.clearTimeout(fadeTimerRef.current)
fadeTimerRef.current = null
}
}, [])
useEffect(() => () => clearTimers(), [clearTimers])
useEffect(() => {
const onMove = (event) => {
@ -25,45 +45,62 @@ export const TooltipProvider = ({ children }) => {
return () => window.removeEventListener('mousemove', onMove)
}, [])
const showTooltip = useCallback((id, nextContent, x, y) => {
if (nextContent == null || nextContent === false || nextContent === '') {
return
}
const showTooltip = useCallback(
(id, nextContent, x, y) => {
if (nextContent == null || nextContent === false || nextContent === '') {
return
}
if (typeof x === 'number' && typeof y === 'number') {
pointRef.current = { x, y }
}
if (typeof x === 'number' && typeof y === 'number') {
pointRef.current = { x, y }
}
if (hideTimerRef.current != null) {
window.clearTimeout(hideTimerRef.current)
hideTimerRef.current = null
}
clearTimers()
stackRef.current = [
...stackRef.current.filter((entry) => entry.id !== id),
{ id, content: nextContent }
]
setContent(nextContent)
}, [])
stackRef.current = [
...stackRef.current.filter((entry) => entry.id !== id),
{ id, content: nextContent }
]
setContent(nextContent)
const hideTooltip = useCallback((id) => {
stackRef.current = stackRef.current.filter((entry) => entry.id !== id)
const last = stackRef.current[stackRef.current.length - 1]
if (!visibleRef.current) {
window.requestAnimationFrame(() => setVisible(true))
} else {
setVisible(true)
}
},
[clearTimers]
)
if (hideTimerRef.current != null) {
window.clearTimeout(hideTimerRef.current)
}
const hideTooltip = useCallback(
(id) => {
stackRef.current = stackRef.current.filter((entry) => entry.id !== id)
const last = stackRef.current[stackRef.current.length - 1]
hideTimerRef.current = window.setTimeout(() => {
hideTimerRef.current = null
setContent(last?.content ?? null)
}, 80)
}, [])
clearTimers()
if (last) {
setContent(last.content)
setVisible(true)
return
}
hideTimerRef.current = window.setTimeout(() => {
hideTimerRef.current = null
setVisible(false)
fadeTimerRef.current = window.setTimeout(() => {
fadeTimerRef.current = null
setContent(null)
}, FADE_OUT_MS)
}, HIDE_DELAY_MS)
},
[clearTimers]
)
return (
<TooltipContext.Provider value={{ showTooltip, hideTooltip }}>
{children}
<CursorTooltip content={content} pointRef={pointRef} />
<CursorTooltip content={content} visible={visible} pointRef={pointRef} />
</TooltipContext.Provider>
)
}