fix(metrics): respect reduced motion in numeric counters - #4522
jackwalkerlabs wants to merge 2 commits into
Conversation
|
@jackwalkerlabs is attempting to deploy a commit to the Umami Software Team on Vercel. A member of the Team first needs to authorize it. |
Greptile SummaryThis PR adds a reactive, hydration-safe reduced-motion preference hook and uses it to bypass numeric counter animations in metric, performance, and list components.
Confidence Score: 4/5The PR appears safe to merge, with a non-blocking compatibility gap for browsers exposing only legacy MediaQueryList listener methods. The reduced-motion behavior and hydration path are coherently implemented and tested; the remaining concern is that the new subscription can throw in legacy browser environments instead of falling back to Files Needing Attention: src/components/hooks/useReducedMotion.ts, src/components/hooks/useReducedMotion.test.tsx
|
| Filename | Overview |
|---|---|
| src/components/hooks/useReducedMotion.ts | Adds a reactive external-store hook with a stable server snapshot, but lacks a fallback for legacy MediaQueryList listener methods. |
| src/components/metrics/ListTable.tsx | Disables count and percentage interpolation under reduced motion while preserving virtualization and explicit animation controls. |
| src/components/metrics/MetricCard.tsx | Jumps springs and directly renders final metric and percentage values when reduced motion is active. |
| src/components/metrics/PerformanceCard.tsx | Bypasses performance-value interpolation for reduced-motion users. |
| src/components/hooks/useReducedMotion.test.tsx | Covers preference updates, cleanup, hydration, and missing matchMedia, but not legacy-only listener APIs. |
| src/components/metrics/reducedMotion.test.tsx | Verifies static reduced-motion output and preservation of existing animated and opt-out paths. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
Preference["prefers-reduced-motion"] --> Hook["useReducedMotion"]
Hook -->|reduce| Static["Render final numeric values"]
Hook -->|no preference| Springs["Use existing spring animations"]
Static --> Metric["MetricCard"]
Static --> Performance["PerformanceCard"]
Static --> List["ListTable"]
Springs --> Metric
Springs --> Performance
Springs --> List
Reviews (1): Last reviewed commit: "fix(metrics): keep motion preferences re..." | Re-trigger Greptile
| media?.addEventListener('change', onChange); | ||
| return () => media?.removeEventListener('change', onChange); |
There was a problem hiding this comment.
Legacy media listeners unsupported
If a browser's MediaQueryList supports only the legacy addListener API, optional chaining on media does not protect this addEventListener call. Mounting a metric component then throws instead of rendering. Feature-detect the listener methods and fall back to addListener and removeListener.
Numeric counters now respect the browser's reduced-motion preference. Metric cards, performance cards and list counts display their final values without counting through intermediate numbers. Normal animation and the existing list
animate={false}option are preserved.Addresses the system-preference part of #3651. This does not add an application settings toggle. The shared media-query hook follows preference changes and uses a stable server snapshot to avoid hydration mismatches.
Validation on
8ecc219f6eb24cb248b7dfe962eed8643fa333df, based ondev9eef6f2:matchMedia. Three counter regressions fail against the original components.pnpm buildpassed with a disposable PostgreSQL database. Next's existing configuration skips TypeScript errors during build: standalonetsc --noEmitstill reports three existing errors, with output identical to unchangeddev(twoRow.flexWraperrors and a deep type instantiation in a 2FA test). Repository lint has the same six baseline errors; all six changed files pass Biome check.Browser checks use actual components in a standalone fixture with synthetic data and localization/mobile adapters, not a complete dashboard workflow. They run through Factory's shipped DockerRunner with a separate RunStore, not its production queue.
Test setup: Node 24.20.0, pnpm 10.30.1, frozen lockfile installation with lifecycle scripts disabled, then Prisma generation and workspace builds. The stock Vitest run rejects the existing Next PostCSS configuration. Unit runs used an operator-only config merging
vitest.config.tswith{ css: { postcss: { plugins: [] } } }; no assertions or test exclusions changed. That override is not part of this PR.Reviewer QA: enable reduced motion in the OS or browser, load a report, and change its data range. Counts, percentages and performance values should update directly. Disable reduced motion and confirm the counters animate again.
AI disclosure: heavy Codex assistance. Codex implemented, reviewed and ran the checks under the submitter's authorization. No independent human review is claimed.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.