File size: 1,887 Bytes
5f24abe | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | import NumberFlow from "number-flow"
import { continuous } from "number-flow/plugins"
import { createEffect, onCleanup, onMount } from "solid-js"
export function RollingNumber(props: {
value: number
target: number
timing: () => { duration: number; easing: string; spinEasing: string }
}) {
let root!: HTMLSpanElement
// Keep the server-rendered text stable while the custom element owns its contents.
const initial = props.value.toLocaleString()
const growing = props.target.toLocaleString().length > initial.length
onMount(() => {
if (new Intl.NumberFormat().resolvedOptions().numberingSystem !== "latn") {
createEffect(() => {
root.textContent = props.value.toLocaleString()
})
return
}
const flow = new NumberFlow()
flow.format = { useGrouping: true, maximumFractionDigits: 0 }
flow.trend = 1
if (growing) flow.plugins = [continuous]
flow.opacityTiming = { duration: 180, easing: "ease-out" }
const motion = window.matchMedia("(prefers-reduced-motion: reduce)")
const updateMotion = () => {
flow.animated = !motion.matches
}
updateMotion()
motion.addEventListener("change", updateMotion)
onCleanup(() => motion.removeEventListener("change", updateMotion))
root.replaceChildren(flow)
createEffect(() => {
const value = props.value
if (flow.value === value) return
const timing = props.timing()
flow.transformTiming = { duration: timing.duration, easing: timing.easing }
flow.spinTiming = {
duration: timing.duration,
easing: growing ? "cubic-bezier(0.45, 0, 0.55, 1)" : timing.spinEasing,
}
flow.update(value)
})
})
return (
<span
data-value
ref={root}
style={{ "min-width": `${props.target.toLocaleString().length}ch`, "text-align": "right" }}
>
{initial}
</span>
)
}
|