diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css index 23b7ad3d..7c94490a 100644 --- a/assets/stylesheets/App.css +++ b/assets/stylesheets/App.css @@ -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), diff --git a/src/components/Dashboard/common/CursorTooltip.jsx b/src/components/Dashboard/common/CursorTooltip.jsx index 36348991..70fb736c 100644 --- a/src/components/Dashboard/common/CursorTooltip.jsx +++ b/src/components/Dashboard/common/CursorTooltip.jsx @@ -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(
{ CursorTooltip.propTypes = { content: PropTypes.node, + visible: PropTypes.bool, pointRef: PropTypes.shape({ current: PropTypes.shape({ x: PropTypes.number, diff --git a/src/components/Dashboard/context/TooltipContext.jsx b/src/components/Dashboard/context/TooltipContext.jsx index 1a760ee1..b5438881 100644 --- a/src/components/Dashboard/context/TooltipContext.jsx +++ b/src/components/Dashboard/context/TooltipContext.jsx @@ -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 ( {children} - + ) }