From f24da6987b95c43a70888245c64ad5cdd83466be Mon Sep 17 00:00:00 2001 From: kurihada Date: Wed, 25 Mar 2026 20:00:09 +0800 Subject: [PATCH] fix(ui): align gauge animation with validation --- packages/ui/src/components/gauge.test.tsx | 4 +++ packages/ui/src/components/gauge.tsx | 29 ++++++++++---------- packages/ui/src/components/progress.test.tsx | 2 +- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/ui/src/components/gauge.test.tsx b/packages/ui/src/components/gauge.test.tsx index a6ba240..fab6d7d 100644 --- a/packages/ui/src/components/gauge.test.tsx +++ b/packages/ui/src/components/gauge.test.tsx @@ -116,6 +116,10 @@ describe("Gauge", () => { const indicator = gauge.querySelector('[data-slot="indicator"]'); const value = gauge.querySelector('[data-slot="value"]'); + act(() => { + vi.advanceTimersByTime(16); + }); + expect(gauge).toHaveAttribute("data-animating", ""); expect(indicator).toHaveAttribute("stroke-dasharray", "0 100"); expect(value).toHaveTextContent("0%"); diff --git a/packages/ui/src/components/gauge.tsx b/packages/ui/src/components/gauge.tsx index ca20e41..e467cbc 100644 --- a/packages/ui/src/components/gauge.tsx +++ b/packages/ui/src/components/gauge.tsx @@ -374,6 +374,7 @@ export const Gauge = forwardRef(function Gauge( useEffect(() => { const targetPercentage = percentage ?? 0; const targetValue = computedAriaValueNow ?? null; + let frame = 0; if ( disableMotion || @@ -383,17 +384,21 @@ export const Gauge = forwardRef(function Gauge( previousPercentageRef.current === targetPercentage && previousValueRef.current === targetValue) ) { - setDisplayPercentage(targetPercentage); - setDisplayValue(targetValue); - setIsAnimating(false); - previousPercentageRef.current = targetPercentage; - previousValueRef.current = targetValue; + frame = window.requestAnimationFrame(() => { + setDisplayPercentage(targetPercentage); + setDisplayValue(targetValue); + setIsAnimating(false); + previousPercentageRef.current = targetPercentage; + previousValueRef.current = targetValue; - if (targetValue != null) { - hasAnimatedRef.current = true; - } + if (targetValue != null) { + hasAnimatedRef.current = true; + } + }); - return; + return () => { + window.cancelAnimationFrame(frame); + }; } const fromPercentage = hasAnimatedRef.current ? previousPercentageRef.current : 0; @@ -401,13 +406,8 @@ export const Gauge = forwardRef(function Gauge( ? previousValueRef.current ?? resolvedMin : resolvedMin; const duration = hasAnimatedRef.current ? UPDATE_SWEEP_DURATION : INITIAL_SWEEP_DURATION; - let frame = 0; let startTime: number | null = null; - setDisplayPercentage(fromPercentage); - setDisplayValue(fromValue); - setIsAnimating(true); - const tick = (timestamp: number) => { if (startTime == null) { startTime = timestamp; @@ -416,6 +416,7 @@ export const Gauge = forwardRef(function Gauge( const progress = Math.min((timestamp - startTime) / duration, 1); const easedProgress = easeOutCubic(progress); + setIsAnimating(progress < 1); setDisplayPercentage(fromPercentage + (targetPercentage - fromPercentage) * easedProgress); setDisplayValue(fromValue + (targetValue - fromValue) * easedProgress); diff --git a/packages/ui/src/components/progress.test.tsx b/packages/ui/src/components/progress.test.tsx index 4db1f62..0db3e52 100644 --- a/packages/ui/src/components/progress.test.tsx +++ b/packages/ui/src/components/progress.test.tsx @@ -110,7 +110,7 @@ describe("Progress", () => { const { rerender } = render(); let progressbar = screen.getByRole("progressbar", { name: "Reduced sync status" }); - let indicator = progressbar.querySelector('[data-slot="indicator"]'); + const indicator = progressbar.querySelector('[data-slot="indicator"]'); expect(progressbar).toHaveAttribute("data-reduced-motion", ""); expect(indicator).toHaveAttribute("data-reduced-motion", "");