From c8bd531bb598eeb6fd726f34f0d85c23c2685580 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Sat, 24 Jan 2026 12:22:57 +0000 Subject: [PATCH] fix(tooltip): exit animation --- src/components/Elements/Tooltip/Tooltip.jsx | 65 ++++++++++++++++++-- src/components/Elements/Tooltip/tooltip.scss | 41 +++++++++++- 2 files changed, 97 insertions(+), 9 deletions(-) diff --git a/src/components/Elements/Tooltip/Tooltip.jsx b/src/components/Elements/Tooltip/Tooltip.jsx index 75696635..5df2df0c 100644 --- a/src/components/Elements/Tooltip/Tooltip.jsx +++ b/src/components/Elements/Tooltip/Tooltip.jsx @@ -4,10 +4,18 @@ import './tooltip.scss'; function Tooltip({ children, title, style, placement, subtitle }) { const [showTooltip, setShowTooltip] = useState(false); + const [isClosing, setIsClosing] = useState(false); const [reference, setReference] = useState(null); const tooltipId = useRef(`tooltip-${Math.random()}`); + const closeTimeout = useRef(null); - const { x, y, refs, strategy } = useFloating({ + const { + x, + y, + refs, + strategy, + placement: computedPlacement, + } = useFloating({ placement: placement || 'bottom', middleware: [flip(), offset(15), shift()], elements: { @@ -15,21 +23,64 @@ function Tooltip({ children, title, style, placement, subtitle }) { }, }); + const handleMouseEnter = () => { + // Clear any pending close timeout if mouse re-enters during exit + if (closeTimeout.current) { + clearTimeout(closeTimeout.current); + closeTimeout.current = null; + } + setIsClosing(false); + setShowTooltip(true); + }; + + const handleMouseLeave = () => { + setIsClosing(true); + // Wait for exit animation to complete before unmounting + closeTimeout.current = setTimeout(() => { + setShowTooltip(false); + setIsClosing(false); + }, 200); // Match exit animation duration + }; + + const handleFocus = () => { + if (closeTimeout.current) { + clearTimeout(closeTimeout.current); + closeTimeout.current = null; + } + setIsClosing(false); + setShowTooltip(true); + }; + + const handleBlur = () => { + setIsClosing(true); + closeTimeout.current = setTimeout(() => { + setShowTooltip(false); + setIsClosing(false); + }, 200); + }; + + // Determine the data-status attribute value + const getStatus = () => { + if (!showTooltip && !isClosing) return 'initial'; + if (isClosing) return 'close'; + return 'open'; + }; + return ( <>
setShowTooltip(true)} - onMouseLeave={() => setShowTooltip(false)} - onFocus={() => setShowTooltip(true)} - onBlur={() => setShowTooltip(false)} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + onFocus={handleFocus} + onBlur={handleBlur} ref={setReference} aria-describedby={tooltipId.current} > {children}
- {showTooltip && ( + {(showTooltip || isClosing) && ( {title} {subtitle} diff --git a/src/components/Elements/Tooltip/tooltip.scss b/src/components/Elements/Tooltip/tooltip.scss index b5a24d4e..4d71c9b1 100644 --- a/src/components/Elements/Tooltip/tooltip.scss +++ b/src/components/Elements/Tooltip/tooltip.scss @@ -33,9 +33,44 @@ cursor: initial; user-select: none; opacity: 1; - animation-name: floating; - animation-duration: 0.3s; - animation-timing-function: ease-in; + transition: + opacity 0.2s ease-out, + transform 0.2s ease-out; + + // Initial state (not yet shown) + &[data-status='initial'] { + opacity: 0; + } + + // Open state - entrance animation + &[data-status='open'] { + opacity: 1; + transform: translate(0, 0); + animation-name: floating; + animation-duration: 0.3s; + animation-timing-function: ease-in; + } + + // Closing state - exit animation + &[data-status='close'] { + opacity: 0; + + &[data-placement^='top'] { + transform: translateY(5px); + } + + &[data-placement^='bottom'] { + transform: translateY(-5px); + } + + &[data-placement^='left'] { + transform: translateX(5px); + } + + &[data-placement^='right'] { + transform: translateX(-5px); + } + } } #modal {