fix(fields): compose the host onBlur in CurrencyField and TagsField - #6858
Merged
Conversation
Both widgets wrote their own `onBlur` AFTER the `{...toDomProps(props)}`
spread, so a host-supplied handler was overridden and never reached the
control. `onBlur` is a DECLARED DOM pass-through key — named in
`FieldWidgetDomProps` and in `SDUI_DOM_PASS_THROUGH_KEYS`, and forwarded by
`toDomProps` — so this is the declared-but-not-delivered class.
Resolve `toDomProps(props)` into `domProps` and call `domProps.onBlur?.(e)`
at the end of the local handler, the idiom the other four widgets of this
package already use.
Not the no-op it was filed as: the form renderer hosts every field through
react-hook-form's `Controller` and spreads the controller field — which
always carries an `onBlur` — into the widget's props, so these two widgets
were opting currency and tags fields out of blur-mode validation on every
form in the repo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…position Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-sam
marked this pull request as ready for review
August 30, 2026 06:52
This was referenced Aug 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6802
CurrencyFieldandTagsFieldeach wrote their ownonBlurAFTER the{...toDomProps(props)}spread, so a host-supplied handler was overridden andnever reached the control.
onBluris a DECLARED DOM pass-through key — namedin
FieldWidgetDomProps(packages/fields/src/widgets/types.ts), named inSDUI_DOM_PASS_THROUGH_KEYS(packages/core/src/utils/dom-props.ts), andforwarded by
toDomProps— so this is the declared-but-not-delivered class(#3290 / #3222).
Both now use the idiom PR #6801 landed for the other three widgets: resolve
toDomProps(props)intodomProps, spread{...domProps}, and calldomProps.onBlur?.(e)at the end of the local handler. No second idiominvented; no public surface widened.
⛔ Read this first — the card's "latent, no-op today" premise is FALSE
The card and its triage both rest on one measurement, taken on
d06059f24:"no host in this repo passes
onBlurto a field widget", which made composinga no-op today and the change a free buy-forward.
Re-measured on this branch's base (
1e14d70), that is not true, and thereproduction is committed as
hostOnBlurDelivery-e2e.test.tsx.The call site. The form renderer hosts every field through
react-hook-form's
Controller— theFormFieldwrapper inpackages/components/src/ui/form.tsxis a thinController— and spreads thecontroller's field object straight into the widget's props:
packages/components/src/renderers/form/form.tsx(the...formFieldspread)A react-hook-form controller field is
{ name, value, onChange, onBlur, ref, disabled }. Measured directly — a probe widget registered into the real formrenderer reported its own prop keys:
So every registered field widget in every form this repo renders is already
handed an
onBlur, and it is the handler that marks the field touched and,under
validationMode'onBlur'/'onTouched', runs its validation.validationModeis authorable onObjectFormSchemaand is wired straight toreact-hook-form's
mode, so metadata alone reaches this.What that means for the change. The defect is not latent — it is active and
user-visible: currency and tags fields were silently opted out of blur-mode
validation while every sibling field type on the same form kept it. The fix is
therefore a REAL behaviour change, not the free no-op the card priced it as.
The direction is unchanged (it restores declared = enforced, and the four
type="number"widgets already behave this way), but the risk note triagewrote — "could start firing onBlur-mode validation on currency fields that
never saw it" — is now the actual, intended effect rather than a hypothetical.
Flagged rather than absorbed, because the reason for doing it now was the
premise that just failed. The changeset says so in the release notes.
Verification
Pre-fix red, with a control (base
1e14d70,hostOnBlurDelivery-e2e.test.tsx):The control passing is what makes the two red rows about the widgets rather
than about the fixture.
Ablation on the committed fix — remove only
domProps.onBlur?.(e)from bothwidgets, run, restore. Mutation confirmed on disk before the run (
grep -cfor the composition:
1 1→0 0; both blob hashes differed from their HEADblobs). No rebuild leg applies: the pins import the widgets by relative source
path and the suite resolves
@object-ui/componentsto source, so nothing isread out of
dist/.PercentField/NumberField/GeolocationFieldand the fourth TagsFieldcase (
works with no host handler at all) stay green under the ablation, asthey should — they do not depend on the removed line. Restore leg proven, not
assumed:
git diff HEADempty and both files byte-identical to their HEADblobs (
07a8ac4…,b4067b7…).Union re-run at the shipping head
d931710:--listFilesconfirms this is a real measurement of the edited files, not agreen that excluded them: all three edited/added test files are in the
tsconfig.test.jsonprogram, and both widgets are in the main program.Gates derived for this diff's paths (
packages/fields/src/**,.changeset/**):check:control-bytes✅ check-control-bytes: OK (scanned 5710 tracked text file(s); skipped 85 binary).check-changeset-presence✅ 5 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)check-changeset-fixed✅ All workspace packages are in the changeset fixed group.check-changeset-no-major✅ No changeset declares a major bump.check-changeset-overwrite✅ No pre-existing changeset was modified or deleted.check:vi-mock-specifiers✅ check-vi-mock-specifiers: OK (3991 tracked source file(s) …)check:i18n-keyspnpm --filter @object-ui/fields lint✖ 907 problems (0 errors, 907 warnings); the package's standing warning baseline. The three files touched here contribute only@typescript-eslint/no-explicit-anywarnings, matching the sibling e2e tests.Declared narrowing. Lint was run package-scoped (
--filter @object-ui/fields), not repo-wide; the repo-wideturbo run lintis CI'srun.
check:sdui-registration-pinsandcheck:eager-closureare recorded asNOT MEASURED, not as passes: both refuse to run without an
apps/consolebuild and say so themselves ("a run with nothing to read has measured
nothing"). Neither reads anything this diff touches; CI builds console and
runs them.
Scope
Both halves of the class, per triage — repairing only
CurrencyFieldwouldhave left
TagsFieldopen with no card tracking it.TagsField'sonBlurISload-bearing in a way the currency one is not (it commits the typed draft as a
tag), so its pin asserts both effects on one blur;
addTagruns first so ablur-mode validator reads the committed list. Currency likewise emits its
rounded value before handing the event on.
The now-false exclusion note in
NumberInputWidgets.badInputAnnounce.test.tsx§5 ("
CurrencyFieldis deliberately absent … no host in this repo passes onetoday") is corrected in place rather than deleted, and
CurrencyFieldjoinsthat file's
it.each.Session (durable copy, since a body edit rewrites the footer):
https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yBGenerated by Claude Code