Skip to content

fix(fields): NumberField reads the published error slot - #7002

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-6803-numberfield-error-slot
Aug 31, 2026
Merged

fix(fields): NumberField reads the published error slot#7002
os-sam merged 2 commits into
mainfrom
claude/issue-6803-numberfield-error-slot

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6803

NumberField destructured { value, onChange, field, readonly, ...props } with
no error, so the published validation slot landed in the open tail and
toDomProps — a whitelist — dropped it. The widget wrote aria-invalid only
while its own bad-input refusal was active.

This wires error and collapses that conditional spread into the ordinary
aria-invalid={!!error || !!refusal} that CurrencyField, PercentField,
LocationField and GeolocationField already use, and adds number to the
WIDGETS table (and the it.each row) of widget-aria-invalid-e2e.test.tsx.

Premise correction — please read before reviewing the pins

The card says an invalid number field "is not announced to assistive tech". That
is true of the widget itself, but it is NOT what a user experiences on any
host in this repo today, and the difference decides which test can go red.

Measured on 1b1d772a5: inside the real form the aria-invalid that
FormControl's Radix Slot hands down arrives as a widget prop, and toDomProps
forwards the whole aria-* family by prefix. So the Slot's correct value
reached the input on its own. Adding number to the #3222 e2e table and running
it against unmodified NumberField gives 14 passed — green before the fix as
well as after. A host was covering for the omission.

That also explains why the registry-wide sweep in
widget-aria-invalid-registry-e2e.test.tsx never caught this with an empty
NOT_YET_DELIVERED ledger: it measures the rendered row, and the row was correct.

So the e2e row alone would have been a pin that passes for the wrong reason. The
load-bearing pin is the consumer half in validation-feedback.test.tsx — the
widget alone, no host, no Slot — which is the widget's own contract and which
does go red. I searched for a host that delivers error without a Slot-provided
aria-invalid; there is none in-repo today (GridField renders a raw Input
for numeric cells, and FieldEditWidget's two callers pass no error), so the
defect is a genuine contract gap that is currently latent rather than a live
screen-reader regression. Triage may want to re-grade the p1.

Red, green, ablation

  1. number added to the e2e table, NumberField untouched: 14 passed — the
    gap is invisible there (above).
  2. The two validation-feedback.test.tsx cases, NumberField untouched:
    2 failedReceived: empty, i.e. no aria-invalid attribute at all.
  3. With the fix: validation-feedback + the 字段 widget 的错误提示键:spec 声明 error,objectui 渲染 errorMessage(declared ≠ enforced) #3222 e2e + badInputAnnounce +
    the registry sweep + environmentDivergence = 5 files, 184 tests passed.
  4. Ablation — revert only the error wiring
    (aria-invalid={!!error || !!refusal} becomes aria-invalid={!!refusal},
    the exact regression the card warns about): 2 failed, one of them the new
    e2e number row. Mutation confirmed on disk by anchored counts in both
    directions plus git hash-object
    (006c352a… HEAD blob vs fc1e6f35… mutated); restored under
    trap ... EXIT INT TERM with absolute paths and verified by hash equality
    back to the HEAD blob plus an empty git diff HEAD.

Step 4 is the answer to "is the overwrite hazard closed": the e2e row is not
load-bearing against today's code, but it is load-bearing against the
un-conditionalised-without-error shape, which is the way this can regress.

Verification

All at 8699b5634:

  • pnpm exec vitest run packages/fields/125 files, 2105 tests passed
  • pnpm exec vitest run packages/plugin-detail/ packages/plugin-kanban/ (the
    two downstream FieldEditWidget hosts) — 136 files, 1191 tests passed
  • pnpm --filter @object-ui/fields type-check — exit 0 (covers the new test
    files: the script runs tsc -p tsconfig.test.json too)
  • pnpm --filter @object-ui/fields lint917 problems (0 errors, 917 warnings), all pre-existing; no narrowing for this package. Type-aware
    linting is not configured, so this diff cannot move the verdict on any file it
    does not touch.
  • check-changeset-presence — "3 source file(s) of 1 released package(s)
    changed, and this change declares 1 changeset(s)"
  • check-control-bytes "OK (scanned 5808 tracked text file(s))",
    check-changeset-fixed, check-changeset-no-major,
    check-changeset-overwrite — all exit 0

check:readme-exports is not measured here, not red: its 306 findings are
all "type entry is not on disk -- run pnpm build first" for packages this
worktree has not built, plus a packagesRead: found 11, floor is 25 refusal.
None names @object-ui/fields, which IS built. It needs a full workspace build,
which CI does.

Scope

Only the three files the card names, plus the changeset. No other widget is
touched and the e2e harness is not refactored. #6780 and #6801 are already
merged and are not affected by this branch.


Generated by Claude Code

`NumberField` destructured `{ value, onChange, field, readonly, ...props }`
with no `error`, so the published validation slot landed in `props` and
`toDomProps` (a whitelist) dropped it. The widget wrote `aria-invalid` only
while its own bad-input refusal was active, so handed an `error` and no host
to cover for it, the control carried no `aria-invalid` at all.
Wire `error` and collapse the conditional spread into the ordinary
`aria-invalid={!!error || !!refusal}` the sibling number widgets use. Both
halves land together: reading `error` is what makes an unconditional
attribute safe, and un-conditionalising is what makes reading `error` visible.
Pinned in `validation-feedback.test.tsx` (the consumer half, which goes red
without the wiring) and `number` added to the `WIDGETS` table of
`widget-aria-invalid-e2e.test.tsx`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-Ci1CiOXr.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NumberField never reads the published error slot, so an invalid number field is not announced (#3222 gap)

2 participants

@os-sam@claude