Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/number-field-fraction-digits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystatic/core': patch
---

Fix `fields.number` rounding values to 3 decimal places when the input is blurred (e.g. `51.98771` was saved as `51.988`)
84 changes: 84 additions & 0 deletions packages/keystatic/src/form/fields/number/ui.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useState } from 'react';
import { expect, test } from 'vitest';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { KeystarProvider } from '@keystar/ui/core';
import { NumberFieldInput } from './ui';

function renderNumberField({
step,
initialValue = null,
}: { step?: number; initialValue?: number | null } = {}) {
const user = userEvent.setup();
const committed: (number | null)[] = [];
function Harness() {
const [value, setValue] = useState<number | null>(initialValue);
return (
<NumberFieldInput
label="Latitude"
description={undefined}
step={step}
validation={undefined}
value={value}
onChange={val => {
committed.push(val);
setValue(val);
}}
autoFocus={false}
forceValidation={false}
/>
);
}
const result = render(
<KeystarProvider locale="en-US">
<Harness />
</KeystarProvider>
);
const input = result.getByRole('textbox') as HTMLInputElement;
return { user, input, committed };
}

test('keeps more than three fraction digits on commit', async () => {
const { user, input, committed } = renderNumberField();
await user.click(input);
await user.keyboard('51.98771');
await user.tab();
expect(committed.at(-1)).toBe(51.98771);
expect(input.value).toBe('51.98771');
});

test('keeps more than three fraction digits of an existing value', async () => {
const { user, input, committed } = renderNumberField({
initialValue: 5.8123456,
});
expect(input.value).toBe('5.8123456');
await user.click(input);
await user.tab();
expect(committed).toEqual([]);
expect(input.value).toBe('5.8123456');
});

test('keeps precision when step is smaller than 0.001', async () => {
const { user, input, committed } = renderNumberField({ step: 0.00001 });
await user.click(input);
await user.keyboard('51.98771');
await user.tab();
expect(committed.at(-1)).toBe(51.98771);
});

test('integers are displayed and committed unchanged', async () => {
const { user, input, committed } = renderNumberField();
await user.click(input);
await user.keyboard('1234567');
await user.tab();
expect(committed.at(-1)).toBe(1234567);
expect(input.value).toBe('1,234,567');
});

test('step still rounds the committed value', async () => {
const { user, input, committed } = renderNumberField({ step: 0.01 });
await user.click(input);
await user.keyboard('1.23456');
await user.tab();
expect(committed.at(-1)).toBe(1.23);
});
10 changes: 10 additions & 0 deletions packages/keystatic/src/form/fields/number/ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ import { useReducer } from 'react';
import { validateNumber } from './validateNumber';
import { FormFieldInputProps } from '../../api';

// NumberField round-trips the committed value through its formatter, and
// Intl.NumberFormat defaults to `maximumFractionDigits: 3`, which would
// silently round stored values (e.g. 51.98771 -> 51.988). 20 is the largest
// value every supported engine accepts, and engines format the shortest
// round-trip representation, so 0.1 still displays as "0.1". Precision is
// then limited only by `step` (if given), not by display formatting.
// Hoisted so the object identity is stable across renders.
const formatOptions: Intl.NumberFormatOptions = { maximumFractionDigits: 20 };

export function NumberFieldInput(
props: FormFieldInputProps<number | null> & {
label: string;
Expand Down Expand Up @@ -33,6 +42,7 @@ export function NumberFieldInput(
onBlur={onBlur}
autoFocus={props.autoFocus}
step={props.step}
formatOptions={formatOptions}
value={props.value === null ? undefined : props.value}
onChange={val => {
props.onChange((val === undefined ? null : val) as any);
Expand Down
Loading