import { useState, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import styles from './progress.module.scss'; const Progress = ({ percentage, strokeWidth = 10, initialAnimation = false, animatePercentageText = false, textForPercentage, colorClassName, isFallback = false, }) => { const [localPercentage, setLocalPercentage] = useState({ percentage: initialAnimation ? 0 : percentage, percentageText: initialAnimation ? 0 : percentage, }); const timeoutId = useRef(); const rafTimerInit = useRef(); const rafCounterTimer = useRef(); const nextTimer = useRef(); useEffect(() => { if (initialAnimation) { timeoutId.current = setTimeout(() => { rafTimerInit.current = window.requestAnimationFrame(() => { setLocalPercentage(prev => ({ ...prev, percentage })); }); }, 0); } return () => { clearTimeout(timeoutId.current); clearTimeout(nextTimer); window.cancelAnimationFrame(rafTimerInit.current); window.cancelAnimationFrame(rafCounterTimer.current); }; /* eslint-disable-next-line */ }, []); useEffect(() => { if (percentage !== localPercentage) { const nextState = { percentage }; if (animatePercentageText) { animateTo(percentage, getTarget(percentage)); } else { nextState.percentageText = percentage; } setLocalPercentage(prev => ({ ...prev, ...nextState })); } /* eslint-disable-next-line */ }, [percentage]); const getTarget = target => { const start = localPercentage.percentageText; const TOTAL_ANIMATION_TIME = 5000; const diff = start > target ? -(start - target) : target - start; const perCycle = TOTAL_ANIMATION_TIME / diff; const cyclesCounter = Math.round( Math.abs(TOTAL_ANIMATION_TIME / perCycle) ); const perCycleTime = Math.round(Math.abs(perCycle)); return { start, target, cyclesCounter, perCycleTime, increment: diff / cyclesCounter, }; }; const animateTo = (percentage, targetState) => { cancelAnimationFrame(rafCounterTimer.current); clearTimeout(nextTimer.current); const current = localPercentage.percentageText; targetState.cyclesCounter--; if (targetState.cyclesCounter <= 0) { setLocalPercentage({ percentageText: targetState.target }); return; } const next = Math.round(current + targetState.increment); rafCounterTimer.current = requestAnimationFrame(() => { setLocalPercentage({ percentageText: next }); nextTimer.current = setTimeout(() => { animateTo(next, targetState); }, targetState.perCycleTime); }); }; const radius = 50 - strokeWidth / 2; const pathDescription = ` M 50,50 m 0,-${radius} a ${radius},${radius} 0 1 1 0,${2 * radius} a ${radius},${radius} 0 1 1 0,-${2 * radius} `; const diameter = Math.PI * 2 * radius; const progressStyle = { strokeDasharray: `${diameter}px ${diameter}px`, strokeDashoffset: `${ ((100 - localPercentage.percentage) / 100) * diameter }px`, }; return isFallback ? ( { // eslint-disable-next-line max-len } ) : ( {localPercentage.percentageText}% ); }; Progress.propTypes = { percentage: PropTypes.number.isRequired, strokeWidth: PropTypes.number, initialAnimation: PropTypes.bool, animatePercentageText: PropTypes.bool, textForPercentage: PropTypes.func, colorClassName: PropTypes.string, isFallback: PropTypes.bool, }; export default Progress;