Thinkmill / Thinkmill/keystatic
fields.number rounds stored values to 3 decimals: formatOptions is never forwarded to NumberField
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 NumberField — label, 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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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