199 lines
5.8 KiB
JavaScript
199 lines
5.8 KiB
JavaScript
import React, { useState, useEffect, useRef } from "react";
|
|
import "./AnimatedText.css";
|
|
|
|
function AnimatedText({
|
|
text,
|
|
transitionDuration = 500,
|
|
transitionDelay = 0,
|
|
style = {},
|
|
animateWidth = true,
|
|
animateEnterenceExitWidth = true,
|
|
}) {
|
|
const [enteringText, setEnteringText] = useState("");
|
|
const [currentText, setCurrentText] = useState(text);
|
|
const [exitingText, setExitingText] = useState("");
|
|
const [isEntering, setIsEntering] = useState(false);
|
|
const [isExiting, setIsExiting] = useState(false);
|
|
const delay = 10;
|
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
|
const isTransitioningRef = useRef(false);
|
|
const prevTextRef = useRef(text);
|
|
const currentTextRef = useRef(currentText);
|
|
const textRef = useRef(text);
|
|
const timeoutRefs = useRef([]);
|
|
const enteringTextRef = useRef(null);
|
|
const [textWidth, setTextWidth] = useState(0);
|
|
const initialLoad = useRef(true);
|
|
|
|
// Helper function to clear all timeouts
|
|
const clearAllTimeouts = () => {
|
|
timeoutRefs.current.forEach((timeoutId) => {
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
});
|
|
timeoutRefs.current = [];
|
|
};
|
|
|
|
useEffect(() => {
|
|
const prevText = prevTextRef.current;
|
|
|
|
// Check if text changed
|
|
const textChanged = prevText != text;
|
|
|
|
// Only trigger transition if text actually changed
|
|
if (textChanged) {
|
|
// Cancel any in-progress transition
|
|
clearAllTimeouts();
|
|
|
|
// Reset transition states immediately
|
|
setIsExiting(false);
|
|
setIsEntering(false);
|
|
setExitingText("");
|
|
setEnteringText("");
|
|
|
|
// Only apply entrance delay when transitioning from "" to text
|
|
const entranceOnly = prevText === "" && text !== "";
|
|
const effectiveDelay = entranceOnly ? transitionDelay : 0;
|
|
|
|
// Get the actual current text (from ref to avoid stale state)
|
|
const actualCurrentText = currentTextRef.current || prevText;
|
|
|
|
// Set exiting text to current text (starts visible)
|
|
setExitingText(actualCurrentText);
|
|
|
|
// Update current text immediately to the new value
|
|
setCurrentText(text);
|
|
currentTextRef.current = text;
|
|
|
|
// Set entering text to new text
|
|
setEnteringText(text);
|
|
|
|
setIsTransitioning(true);
|
|
isTransitioningRef.current = true;
|
|
|
|
// Start exit animation (no delay - exit is immediate)
|
|
const exitTimeout = setTimeout(() => {
|
|
setIsExiting(true);
|
|
}, delay);
|
|
timeoutRefs.current.push(exitTimeout);
|
|
|
|
// Start enter animation (with delay only for entrance: "" to text)
|
|
const enterTimeout = setTimeout(() => {
|
|
setIsEntering(true);
|
|
}, delay + effectiveDelay);
|
|
timeoutRefs.current.push(enterTimeout);
|
|
|
|
// Clear exiting text after transition completes
|
|
const cleanupTimeout = setTimeout(
|
|
() => {
|
|
// When animateEnterenceExitWidth is false, set width after effect has finished (before clearing ref)
|
|
if (!animateEnterenceExitWidth && enteringTextRef.current) {
|
|
setTextWidth(enteringTextRef.current.clientWidth);
|
|
}
|
|
setIsExiting(false);
|
|
setIsEntering(false);
|
|
setExitingText("");
|
|
setEnteringText("");
|
|
setIsTransitioning(false);
|
|
isTransitioningRef.current = false;
|
|
},
|
|
transitionDuration + effectiveDelay + delay,
|
|
);
|
|
timeoutRefs.current.push(cleanupTimeout);
|
|
|
|
prevTextRef.current = text;
|
|
} else {
|
|
// Update text without transition for updates without change
|
|
currentTextRef.current = text;
|
|
setEnteringText("");
|
|
setExitingText("");
|
|
setIsEntering(false);
|
|
setIsExiting(false);
|
|
setIsTransitioning(false);
|
|
isTransitioningRef.current = false;
|
|
setCurrentText(text);
|
|
}
|
|
|
|
// Cleanup function
|
|
return () => {
|
|
clearAllTimeouts();
|
|
};
|
|
}, [text, transitionDuration, transitionDelay, animateEnterenceExitWidth]);
|
|
|
|
useEffect(() => {
|
|
if (enteringTextRef.current) {
|
|
if (animateWidth && isTransitioning) {
|
|
setTextWidth(enteringTextRef.current.clientWidth);
|
|
}
|
|
}
|
|
if (
|
|
(!animateWidth || !animateEnterenceExitWidth) &&
|
|
!isTransitioning &&
|
|
textRef.current
|
|
) {
|
|
setTextWidth(textRef.current.clientWidth);
|
|
}
|
|
if (initialLoad.current == true && textRef.current) {
|
|
initialLoad.current = false;
|
|
setTextWidth(textRef.current.clientWidth);
|
|
}
|
|
}, [enteringText, animateWidth, animateEnterenceExitWidth, isTransitioning]);
|
|
|
|
// Keep currentTextRef in sync
|
|
useEffect(() => {
|
|
currentTextRef.current = currentText;
|
|
}, [currentText]);
|
|
|
|
const transitionStyle = {
|
|
transition: `opacity ${transitionDuration}ms ease, transform ${transitionDuration}ms ease`,
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="animated-text-container"
|
|
style={{
|
|
...style,
|
|
width: textWidth,
|
|
transition:
|
|
animateWidth && animateEnterenceExitWidth
|
|
? `width ${transitionDuration}ms ease`
|
|
: undefined,
|
|
}}
|
|
>
|
|
<div
|
|
className={`animated-text animated-text-current ${
|
|
isTransitioning == true ? "animated-text-hidden" : ""
|
|
}`}
|
|
ref={textRef}
|
|
>
|
|
{currentText}
|
|
</div>
|
|
{exitingText && (
|
|
<div
|
|
className={`animated-text animated-text-exiting ${
|
|
isExiting == true ? "animated-text-exiting-visible" : ""
|
|
}`}
|
|
style={transitionStyle}
|
|
>
|
|
{exitingText}
|
|
</div>
|
|
)}
|
|
|
|
{enteringText && (
|
|
<div
|
|
className={`animated-text animated-text-entering ${
|
|
isEntering == true ? "animated-text-entering-visible" : ""
|
|
}`}
|
|
style={transitionStyle}
|
|
ref={enteringTextRef}
|
|
>
|
|
{enteringText}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default AnimatedText;
|