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 (
{currentText}
{exitingText && (
{exitingText}
)} {enteringText && (
{enteringText}
)}
); } export default AnimatedText;