Environment
packages/0 master (worktree at feat/builder, identical NumberField source).
Repro
- Bind a NumberField to parent state:
<NumberField.Root v-model="state.breakpoints[name]"> (e.g. the builder app's Breakpoints form, apps/builder/src/plugins/breakpoints/BreakpointsConfig.vue).
- Focus the control, type a new value (e.g.
1000), then blur or press Enter.
- The field snaps back to the previous value; the parent model never receives the typed value.
ArrowUp/ArrowDown stepping and programmatic writes propagate fine — only the typed parse→commit path is broken, and only when the model is parent-bound (plain local refs take defineModel's local-value path and don't reproduce, which is why unit tests pass).
Mechanism
NumberFieldControl.vue onInput updates only text.value, not the model.
- On blur/Enter (
NumberFieldControl.vue:102-108, :128-132): root.value.value = parsed then root.commit().
root.value is the defineModel ref (NumberFieldRoot.vue:227). With a parent listener, the write emits update:modelValue, but the child's prop hasn't round-tripped yet — a synchronous re-read returns the stale value.
commit() (createNumberField/index.ts:208-219) re-reads value.value synchronously and re-assigns value.value = numeric.snap(value.value) — re-emitting the old number and clobbering the parse in the same tick. syncText() then restores the old text.
The existing tests document the flake instead of catching it: packages/0/src/components/NumberField/index.test.ts:1568 and :1588 accept either the new or the old value.
Proposed fix
Thread the parsed value through instead of re-reading the model:
// NumberFieldControl.vue (blur + Enter)
const parsed = root.parse(text.value)
root.value.value = parsed
root.commit(parsed)
// createNumberField/index.ts
function commit (next?: number | null): void {
const val = next ?? value.value
if (isNull(val)) return
// ...range/snap logic on `val`...
value.value = numeric.snap(val)
}
Regression test: mount with a parent-bound v-model, type + blur, assert the parent ref received the typed value (and tighten the two either/or assertions at index.test.ts:1568/1588).
Worth grepping sibling composables (createSlider, createInput, createRating) for the same write-then-synchronously-re-read-model shape while fixing.
Impact
Any consumer binding NumberField through parent state cannot type values — discovered because custom breakpoint px values are impossible to enter in the framework builder.
Environment
packages/0master (worktree at feat/builder, identical NumberField source).Repro
<NumberField.Root v-model="state.breakpoints[name]">(e.g. the builder app's Breakpoints form,apps/builder/src/plugins/breakpoints/BreakpointsConfig.vue).1000), then blur or press Enter.ArrowUp/ArrowDown stepping and programmatic writes propagate fine — only the typed parse→commit path is broken, and only when the model is parent-bound (plain local refs take
defineModel's local-value path and don't reproduce, which is why unit tests pass).Mechanism
NumberFieldControl.vueonInputupdates onlytext.value, not the model.NumberFieldControl.vue:102-108,:128-132):root.value.value = parsedthenroot.commit().root.valueis thedefineModelref (NumberFieldRoot.vue:227). With a parent listener, the write emitsupdate:modelValue, but the child's prop hasn't round-tripped yet — a synchronous re-read returns the stale value.commit()(createNumberField/index.ts:208-219) re-readsvalue.valuesynchronously and re-assignsvalue.value = numeric.snap(value.value)— re-emitting the old number and clobbering the parse in the same tick.syncText()then restores the old text.The existing tests document the flake instead of catching it:
packages/0/src/components/NumberField/index.test.ts:1568and:1588accept either the new or the old value.Proposed fix
Thread the parsed value through instead of re-reading the model:
Regression test: mount with a parent-bound v-model, type + blur, assert the parent ref received the typed value (and tighten the two either/or assertions at
index.test.ts:1568/1588).Worth grepping sibling composables (createSlider, createInput, createRating) for the same write-then-synchronously-re-read-model shape while fixing.
Impact
Any consumer binding NumberField through parent state cannot type values — discovered because custom breakpoint px values are impossible to enter in the framework builder.