From 0a9edcb400d835617f6a1d18abae22bd61453748 Mon Sep 17 00:00:00 2001 From: Bartosz-Do Date: Fri, 25 Sep 2026 15:34:35 +0200 Subject: [PATCH] Fix CountUp printing `-0` when a value crosses or rounds to zero (#3221) Co-authored-by: codecalm --- .changeset/countup-negative-zero.md | 5 +++++ core/js/src/countup.ts | 12 ++++++---- core/js/tests/unit/countup.spec.ts | 34 +++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 .changeset/countup-negative-zero.md diff --git a/.changeset/countup-negative-zero.md b/.changeset/countup-negative-zero.md new file mode 100644 index 000000000..e44b21922 --- /dev/null +++ b/.changeset/countup-negative-zero.md @@ -0,0 +1,5 @@ +--- +"@tabler/core": patch +--- + +Fixed CountUp printing `-0` when a value crosses or rounds to zero. diff --git a/core/js/src/countup.ts b/core/js/src/countup.ts index a980b350c..5f22ddc56 100644 --- a/core/js/src/countup.ts +++ b/core/js/src/countup.ts @@ -331,14 +331,18 @@ class CountUp extends BaseComponent { } if (format === 'time') { - const minutes = Math.round(Math.abs(value)) - return `${value < 0 ? '-' : ''}${prefix}${Math.floor(minutes / 60)}:${String(minutes % 60).padStart(2, '0')}${suffix}` + // Rounded away from zero on both sides, so -90.5 and 90.5 give the same + // minutes; a value that rounds to 0 stays unsigned (-0 < 0 is false). + const rounded = Math.sign(value) * Math.round(Math.abs(value)) + const minutes = Math.abs(rounded) + return `${rounded < 0 ? '-' : ''}${prefix}${Math.floor(minutes / 60)}:${String(minutes % 60).padStart(2, '0')}${suffix}` } - const [integer, fraction] = Math.abs(value).toFixed(decimalPlaces).split('.') as [string, string | undefined] + const fixed = value.toFixed(decimalPlaces) + const [integer, fraction] = fixed.replace('-', '').split('.') as [string, string | undefined] const grouped = useGrouping ? integer.replace(/\B(?=(\d{3})+(?!\d))/g, separator) : integer - return `${value < 0 ? '-' : ''}${prefix}${grouped}${fraction ? decimal + fraction : ''}${suffix}` + return `${Number(fixed) < 0 ? '-' : ''}${prefix}${grouped}${fraction ? decimal + fraction : ''}${suffix}` } } diff --git a/core/js/tests/unit/countup.spec.ts b/core/js/tests/unit/countup.spec.ts index 16aa5b5de..f5fea6271 100644 --- a/core/js/tests/unit/countup.spec.ts +++ b/core/js/tests/unit/countup.spec.ts @@ -239,6 +239,15 @@ describe('CountUp', () => { instance.reset() expect(element().textContent).toBe('50,000') }) + + it('should not print a negative zero when a value rounds to zero', async () => { + fixtureEl.innerHTML = '

-0.3

' + + quick({ decimalPlaces: 0 }) + await wait(300) + + expect(element().textContent).toBe('0') + }) }) describe('format', () => { @@ -262,6 +271,31 @@ describe('CountUp', () => { expect(element().textContent).toBe('9999999999') }) + it('should not print a negative zero when a time rounds to zero minutes', async () => { + fixtureEl.innerHTML = '

0:00

' + + const instance = new CountUp(element(), { autoAnimate: false, duration: 0.1, format: 'time' }) + await wait(300) + instance.update(-0.4) + await wait(300) + + expect(element().textContent).toBe('0:00') + }) + + it('should round negative half minutes like positive ones in the time format', async () => { + fixtureEl.innerHTML = '

0:00

' + + const instance = new CountUp(element(), { autoAnimate: false, duration: 0.1, format: 'time' }) + await wait(300) + instance.update(90.5) + await wait(300) + expect(element().textContent).toBe('1:31') + + instance.update(-90.5) + await wait(300) + expect(element().textContent).toBe('-1:31') + }) + it('should use a formatter function from the config', async () => { quick({ formatter: (value: number) => `${Math.round(value / 1000)}k` }) await wait(300)