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; 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 { .cursor-tooltip .ant-card {
box-shadow: box-shadow:
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 6px 16px 0 rgba(0, 0, 0, 0.08),

View File

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

View File

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