fix(tooltip): exit animation

This commit is contained in:
alexsparkes
2026-01-24 12:22:57 +00:00
parent 2ded256bda
commit c8bd531bb5
2 changed files with 97 additions and 9 deletions

View File

@@ -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 (
<>
<div
className="tooltip"
style={style}
onMouseEnter={() => 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}
</div>
{showTooltip && (
{(showTooltip || isClosing) && (
<span
ref={refs.setFloating}
style={{
@@ -40,6 +91,8 @@ function Tooltip({ children, title, style, placement, subtitle }) {
flexFlow: 'column',
}}
className="tooltipTitle"
data-status={getStatus()}
data-placement={computedPlacement}
>
{title}
<span style={{ fontSize: '8px' }}>{subtitle}</span>

View File

@@ -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 {