This repository has been archived on 2026-10-08. You can view files and clone it, but cannot push or open issues or pull requests.

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;