Files
openchamber/packages
Leonidandbashrusakh 4e591503ce fix(number-input): stepper drift on rapid clicks (closes #2053) (#2059)
* fix(number-input): stepper drift on rapid clicks (closes #2053)

The shared NumberInput stepper buttons (-/+) computed the next value from
`baseValue`, a useMemo of the controlled `value` prop. When the user
pressed - and + in rapid succession, both inline closures read the same
pre-update `baseValue` because the prop round-trip (click ->
onValueChange -> store/persistence update -> re-render) had not landed.
Net result: rapid alternation drifted or oscillated instead of returning
to the start value.

Route the stepper math through a new `committedValueRef` updated
synchronously inside `commitValue`, and re-sync the ref from `baseValue`
via a useEffect so external mutations (the reset button next to each
stepper, undo, multi-instance sync) keep the ref aligned. Keep
`baseValue` for the `disabled` predicate so the prop still gates the
buttons at the bounds.

For the same invariant, route `handleBlur`'s finite-parse branch
through `commitValue` so a typed value followed by a stepper click does
not compute from a stale ref. The empty-draft `onClear` early-return
relies on the baseValue useEffect to re-sync.

Cover the path with a new bun:test suite that drives the real onClick
closures through createRoot with a minimal document/window stub (no new
deps). Tests assert rapid --+ and +-- sequences net to the start value,
a sustained 6-click alternation does not drift, sequential clicks with a
re-render between them settle correctly, and a typed-then-stepper
sequence uses the typed base.

* test(number-input): restore DOM globals and guard empty recorded arrays

Follow-up to the stepper-drift fix on the same PR.

- installDomStub now captures the previous values of
  document/window/navigator/IS_REACT_ACT_ENVIRONMENT before overwriting
  and exposes a restore() function. withHandle calls stub.restore() in
  finally after unmount(), so the test process no longer leaks a fake
  DOM across tests.
- Replace the four 'recorded[length-1]!' non-null assertions with a
  lastCommit(handle) helper that throws a clear error if the parent
  never produced a commit. A regression that drops the first commit
  fails loudly instead of silently coercing to undefined.
- Add a short comment on the useEffect re-sync documenting the
  controlled-parent assumption (ref can briefly lead the prop if a
  parent ever rejects or debounces onValueChange; no production caller
  does today).

---------

Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
2026-07-11 14:49:19 +03:00
..
2026-07-10 19:41:49 +03:00
2026-07-10 19:41:49 +03:00
2026-07-10 19:41:49 +03:00