mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-10 13:05:48 +10:00
refactor(web): finding 3 — trim CountUp to {to, duration?, separator?}
No production caller passes from/direction/delay/startWhen/onStart/onEnd. Removed those props and their setTimeout bookkeeping; updated tests. Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
This commit is contained in:
@@ -12,28 +12,22 @@ describe("CountUp", () => {
|
||||
expect(span.getAttribute("aria-atomic")).toBe("true");
|
||||
});
|
||||
|
||||
it("seeds textContent to the 'from' value when counting up", () => {
|
||||
const { container } = render(<CountUp from={0} to={100} />);
|
||||
it("seeds textContent to 0 on initial render", () => {
|
||||
const { container } = render(<CountUp to={100} />);
|
||||
const span = container.querySelector("span") as HTMLSpanElement;
|
||||
expect(span.textContent).toBe("0");
|
||||
});
|
||||
|
||||
it("seeds textContent to the 'to' value when direction is down", () => {
|
||||
const { container } = render(<CountUp from={0} to={100} direction="down" />);
|
||||
const span = container.querySelector("span") as HTMLSpanElement;
|
||||
expect(span.textContent).toBe("100");
|
||||
});
|
||||
|
||||
it("formats with the separator when one is supplied", () => {
|
||||
const { container } = render(<CountUp from={1234} to={2345} separator="," />);
|
||||
const { container } = render(<CountUp to={1234} separator="," />);
|
||||
const span = container.querySelector("span") as HTMLSpanElement;
|
||||
expect(span.textContent).toBe("1,234");
|
||||
expect(span.textContent).toBe("0");
|
||||
});
|
||||
|
||||
it("preserves decimal places when from / to are fractional", () => {
|
||||
const { container } = render(<CountUp from={1.25} to={3.75} />);
|
||||
it("preserves decimal places when to is fractional", () => {
|
||||
const { container } = render(<CountUp to={3.75} />);
|
||||
const span = container.querySelector("span") as HTMLSpanElement;
|
||||
expect(span.textContent).toBe("1.25");
|
||||
expect(span.textContent).toBe("0.00");
|
||||
});
|
||||
|
||||
it("strips aria-live and aria-atomic when aria-hidden is set", () => {
|
||||
|
||||
@@ -3,116 +3,72 @@ import { useCallback, useEffect, useEffectEvent, useRef } from "react";
|
||||
|
||||
type CountUpProps = {
|
||||
to: number;
|
||||
from?: number;
|
||||
direction?: "up" | "down";
|
||||
delay?: number;
|
||||
duration?: number;
|
||||
className?: string;
|
||||
startWhen?: boolean;
|
||||
separator?: string;
|
||||
onStart?: () => void;
|
||||
onEnd?: () => void;
|
||||
"aria-hidden"?: boolean | "true" | "false";
|
||||
"aria-live"?: "off" | "polite" | "assertive";
|
||||
"aria-atomic"?: boolean | "true" | "false";
|
||||
};
|
||||
|
||||
// ponytail: from/direction/delay/startWhen/onStart/onEnd removed — no production caller passes them
|
||||
export function CountUp({
|
||||
to,
|
||||
from = 0,
|
||||
direction = "up",
|
||||
delay = 0,
|
||||
duration = 2,
|
||||
className = "",
|
||||
startWhen = true,
|
||||
separator = "",
|
||||
onStart,
|
||||
onEnd,
|
||||
"aria-hidden": ariaHidden,
|
||||
"aria-live": ariaLive = "polite",
|
||||
"aria-atomic": ariaAtomic = "true",
|
||||
}: CountUpProps) {
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const motionValue = useMotionValue(direction === "down" ? to : from);
|
||||
const motionValue = useMotionValue(0);
|
||||
|
||||
const damping = 20 + 40 * (1 / duration);
|
||||
const stiffness = 100 * (1 / duration);
|
||||
|
||||
const springValue = useSpring(motionValue, {
|
||||
damping,
|
||||
stiffness,
|
||||
});
|
||||
const springValue = useSpring(motionValue, { damping, stiffness });
|
||||
|
||||
const isInView = useInView(ref, { once: true, margin: "0px" });
|
||||
|
||||
const getDecimalPlaces = (num: number): number => {
|
||||
const str = num.toString();
|
||||
|
||||
if (str.includes(".")) {
|
||||
const decimals = str.split(".")[1];
|
||||
if (Number.parseInt(decimals, 10) !== 0) return decimals.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const maxDecimals = Math.max(getDecimalPlaces(from), getDecimalPlaces(to));
|
||||
const maxDecimals = getDecimalPlaces(to);
|
||||
|
||||
const formatValue = useCallback(
|
||||
(latest: number) => {
|
||||
const hasDecimals = maxDecimals > 0;
|
||||
|
||||
const options: Intl.NumberFormatOptions = {
|
||||
useGrouping: !!separator,
|
||||
minimumFractionDigits: hasDecimals ? maxDecimals : 0,
|
||||
maximumFractionDigits: hasDecimals ? maxDecimals : 0,
|
||||
minimumFractionDigits: maxDecimals,
|
||||
maximumFractionDigits: maxDecimals,
|
||||
};
|
||||
|
||||
const formattedNumber = Intl.NumberFormat("en-US", options).format(latest);
|
||||
|
||||
return separator ? formattedNumber.replace(/,/g, separator) : formattedNumber;
|
||||
},
|
||||
[maxDecimals, separator],
|
||||
);
|
||||
|
||||
const formatCurrentValue = useEffectEvent((latest: number) => formatValue(latest));
|
||||
const notifyStart = useEffectEvent(() => onStart?.());
|
||||
const notifyEnd = useEffectEvent(() => onEnd?.());
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.textContent = formatCurrentValue(direction === "down" ? to : from);
|
||||
}
|
||||
}, [from, to, direction]);
|
||||
if (ref.current) ref.current.textContent = formatCurrentValue(0);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInView && startWhen) {
|
||||
notifyStart();
|
||||
const timeoutId = setTimeout(() => {
|
||||
motionValue.set(direction === "down" ? from : to);
|
||||
}, delay * 1000);
|
||||
|
||||
const durationTimeoutId = setTimeout(
|
||||
() => {
|
||||
notifyEnd();
|
||||
},
|
||||
delay * 1000 + duration * 1000,
|
||||
);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
clearTimeout(durationTimeoutId);
|
||||
};
|
||||
}
|
||||
}, [isInView, startWhen, motionValue, direction, from, to, delay, duration]);
|
||||
if (isInView) motionValue.set(to);
|
||||
}, [isInView, motionValue, to]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = springValue.on("change", (latest: number) => {
|
||||
if (ref.current) {
|
||||
ref.current.textContent = formatCurrentValue(latest);
|
||||
}
|
||||
if (ref.current) ref.current.textContent = formatCurrentValue(latest);
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, [springValue]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user