Skip to content

fix(InputNumber): work uncontrolled with only a default value (nuxt/ui@2d4782b) - #552

Merged
IgorShevchik merged 2 commits into
mainfrom
sync/nuxt-2d4782ba
Sep 8, 2026
Merged

fix(InputNumber): work uncontrolled with only a default value (nuxt/ui@2d4782b)#552
IgorShevchik merged 2 commits into
mainfrom
sync/nuxt-2d4782ba

Conversation

@IgorShevchik

Copy link
Copy Markdown
Collaborator

Port of nuxt/ui@2d4782ba — third of ten in this run.

The defect, and it was live here

useVModel(props, 'modelValue', emits, { defaultValue }) kept its own copy of the value, and that copy was bound to NumberFieldRoot's model-value. With only a defaultValue and no v-model, reka has internal state of its own — and the two disagreed. The field stepped off the wrapper's copy instead of off reka's, so an uncontrolled InputNumber did not increment at all.

Removing the wrapper alone trades that for a different bug, which is why the fix has a second half. Reka writes update:modelValue on every write — blur, Enter, stepping at a bound — whether or not the value moved, and with useVModel gone nothing absorbs those. So onUpdate gains an equality guard and emits by hand:

if (value === props.modelValue || (value == null && props.modelValue == null)) {
  return
}

emits('update:modelValue', value as ApplyModifiers<T, Mod>)

The second clause is not redundant: undefined and null are different values but the same emptiness — the .optional modifier produces the first, a caller supplies the second. useForwardProps(…, emits) loses its second argument for the same reason, so the root cannot re-emit what onUpdate now owns.

Applies verbatim. Our pre-image is upstream's at all five sites, eslint-disable comment included, useVModel appears nowhere else in the file, and the selectors and imports the new tests need (data-slot="increment", flushPromises, test) were already present.

Verified against the pre-image, not assumed

With the component reverted and upstream's four tests kept, all four fail; with the port, all pass. So this fixes a defect that was live in this fork rather than a theoretical one.

Mutations, and the counts separate the two halves of the fix:

mutation result
drop the emits('update:modelValue', …) call 14 red
drop the equality guard entirely 6 red
keep only value === props.modelValue, drop the == null clause 2 red

The last row is the one worth having: it shows the == null clause is load-bearing and covered, rather than defensive padding no test would miss.

Bookkeeping

Local gate green: lint · typecheck · build (3.87 MB) · test (347 files, 7895 passed, 6 skipped) · test:module · repl:build.

Still ahead

Seven upstream commits: 042bf3b7 (tiptap deps), 7224333b (upstream's own release), 5fd94e13 (theme editor docs), 9076ca2d (virtualizer size fallback), and three docs-only commits.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JS8ypVfQSFzYVZzkTHhURb


Generated by Claude Code

…2d4782b)

`useVModel` kept its own copy of the value and that copy was bound to
`NumberFieldRoot`'s `model-value`. With only a `defaultValue` and no `v-model`,
reka has internal state of its own and the two disagreed — the field stepped off
the wrapper's copy instead of off reka's, so an uncontrolled `InputNumber` did
not increment at all.

Removing the wrapper alone trades that for a different bug, which is why the fix
has a second half. Reka writes `update:modelValue` on every write — blur, Enter,
stepping at a bound — whether or not the value moved, and with `useVModel` gone
nothing absorbs those. So `onUpdate` gains an equality guard and emits by hand,
and `useForwardProps` loses its `emits` argument for the same reason: the root
must not re-emit what `onUpdate` now owns.

The guard's second clause is not redundant. `undefined` and `null` are different
values but the same emptiness — the `.optional` modifier produces the first, a
caller supplies the second.

Verified against the pre-image rather than assumed: with the component reverted
and upstream's four tests kept, all four fail; with the port, all pass. The
defect was live here.

Mutations separate the two halves — dropping the emit turns 14 red, dropping the
guard turns 6 red, and keeping only `value === props.modelValue` while dropping
the `== null` clause turns 2 red. That last one shows the clause is load-bearing
rather than defensive padding no test would miss.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JS8ypVfQSFzYVZzkTHhURb
`ebd4adfc` went in with `pending-merge` in both fields, as PORTING.md §6 step 4
requires. That PR has now merged:

- `ebd4adfc` → #551 / `d57db682`

Carried here rather than in its own bookkeeping PR, per §6 step 4: the next port
in a run reconciles the previous entry, and only the last entry needs a PR of
its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JS8ypVfQSFzYVZzkTHhURb
@IgorShevchik
IgorShevchik merged commit 704312d into main Sep 8, 2026
2 checks passed
@IgorShevchik
IgorShevchik deleted the sync/nuxt-2d4782ba branch September 8, 2026 10:14
IgorShevchik added a commit that referenced this pull request Sep 8, 2026
…42bf3b) (#553)

* chore(deps): update tiptap to ^3.31.3, pin prosemirror-view (nuxt/ui@042bf3b)

All 25 `@tiptap/*` declarations across five manifests sat at `^3.30.5`,
upstream's exact pre-image, `playgrounds/demo` included. The `@tiptap/pm`
override moves with the family, as its own comment instructs.

The bump broke `typecheck` in all three copies of the editor completion
extension:

    TS2322: Type '(state: EditorState) => DecorationSet' is not assignable to
    '(state: EditorState) => DecorationSource | null | undefined'

That reads like an API change and is not one. It is two copies of
`prosemirror-view`, so `DecorationSet` comes from one and `DecorationSource`
from the other and the two are unrelated types.

Measured against `main`: `prosemirror-view` was 1.42.0 alone, and after the bump
1.42.0 *and* 1.42.3. `@tiptap/pm@3.31.3` wants 1.42.3, while
`prosemirror-dropcursor`, `-gapcursor`, `-history`, `-state` and `-tables`
peer-accept a range and had settled on 1.42.0.

This is the failure the `@tiptap/pm` override exists to prevent, arriving one
level down — and that override could not prevent it, because pinning the tiptap
wrapper keeps one wrapper, not one `prosemirror-view`. So it gains a sibling,
with the reasoning beside it. A single 1.42.3 afterwards, and types are clean.

`@tiptap/core` resolves to two versions and that is not this commit's doing:
`main` already had 3.30.2 and 3.30.5. The older line lives entirely under
`@nuxt/ui@4.8.2`, reached through `nuxtseo-layer-devtools` — a devtools
dependency that never touches our `Editor`. No importer references it. Recorded
so a reader comparing counts does not suspect this change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JS8ypVfQSFzYVZzkTHhURb

* chore(sync): reconcile the #552 ledger entry

`2d4782ba` went in with `pending-merge` in both fields, as PORTING.md §6 step 4
requires. That PR has now merged:

- `2d4782ba` → #552 / `704312db`

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JS8ypVfQSFzYVZzkTHhURb

---------

Co-authored-by: Shevchik Igor <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants