Thinkmill / Thinkmill/keystatic

fields.number rounds stored values to 3 decimals: formatOptions is never forwarded to NumberField

Open
#1,614 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
2.4k
Forks
159
Avg merge
21h 41m
Merged PRs (30d)
2

Description

What happens

fields.number silently rounds committed values to 3 decimal places. Typing 51.98600 stores 51.986. This makes the field unusable for latitude/longitude: 0.001° is ~111 m of latitude, so a map marker lands on the wrong block.

Reproduced on @keystatic/core@0.5.48; the relevant code is unchanged in 0.6.8.

Why

NumberFieldInput passes nine props to @keystar/ui's NumberFieldlabel, description, isRequired, errorMessage, onBlur, autoFocus, step, value, onChange — and no formatOptions:

// dist/keystatic-core.js:2476 (0.5.48), :2455ff (0.6.8) — inside NumberFieldInput
jsx(NumberField, { label, description, isRequired, errorMessage, onBlur, autoFocus, step, value, onChange })

(Corrected 2026-08-25: this block first cited dist/index-b09c5e84.js:58 and an eight-prop list without step — that is IntegerFieldInput, a different component that also renders NumberField. See the correction comment below; the conclusion is unchanged.)

NumberField accepts formatOptions (@keystar/ui/src/number-field/types.tsx) and forwards its props to useNumberFieldState, whose commit() round-trips the value (not just the display text) through the formatter:

// @react-stately/numberfield
clampedValue = numberParser.parse(format(clampedValue));
setNumberValue(clampedValue);

With no formatOptions, format is an Intl.NumberFormat with the default maximumFractionDigits: 3. So the rounding is in the stored value, and it is locale-independent — that default is the same in every locale for style: 'decimal'.

Why step is not the workaround

NumberFieldInput does forward step to NumberField, but useNumberFieldState derives its NumberFormatter/NumberParser from formatOptions alone (useNumberFieldState.mjs:33-47), and snapValueToStep runs before the parse(format(...)) line above — so a step of 0.00001 is still rounded back to 3 decimals. formatOptions appears to be the only lever.

IntegerFieldInput omits formatOptions as well; harmless for integers, but both call sites would want the prop.

Suggested fix

Expose formatOptions?: Intl.NumberFormatOptions on fields.number and forward it to NumberField (a hoisted/memoized value, since useNumberFieldState compares formatOptions by identity and resets inputValue when it changes — an inline literal would clobber input while typing).

A narrower alternative would be to derive maximumFractionDigits from step when one is given, which would make the existing step option meaningful for the UI.

Happy to open a PR if you'd like it in either shape.

Context

Community platform for a Dutch municipality where non-technical editors enter organisation coordinates through the CMS. The editor reported it as "the field only accepts 3 decimals" — the field description we had written told them to enter 5.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with NumberFieldInput and IntegerFieldInput, then read @keystar/ui/src/number-field/types.tsx and the described useNumberFieldState formatting behavior. The change is complete when fields.number exposes and forwards formatOptions without disrupting typing, and committed values retain the requested decimal precision.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
frontend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
74/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.