Skip to content

fix(components): enforce the declared max_length ceiling on the form default fallback branch - #5276

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-5253-form-default-branch-ceiling
Aug 18, 2026
Merged

fix(components): enforce the declared max_length ceiling on the form default fallback branch#5276
os-support-ai merged 1 commit into
mainfrom
claude/issue-5253-form-default-branch-ceiling

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes#5253

The last arm of the built-in form's field switch — the default fallback, serving a type that is neither a BUILTIN_FIELD_TYPES member nor resolvable from the registry under field: + type or the bare type — spread its props straight onto the rendered Input and never read the declared ceiling. It is the same defect #5201 fixed one arm earlier in the same switch, and it survived that card because #5201's triage ruling scoped the work to the input branch.

Measured before-state

Re-measured on current main (87d9202b1, i.e. after#5201's PR merged), rendering the built-in branch with no registerAllFields() and a field of type: 'zzunknown', dumping getAttributeNames() and getAttribute('maxlength'):

max_length: 50 → attrs=["class","max_length","id","aria-describedby","aria-invalid","type","name"] maxlength=null max_length="50"
maxLength: 50 → attrs=["class","maxlength","id","aria-describedby","aria-invalid","type","name"] maxlength="50"
both (50/80) → attrs=["class","maxlength","max_length","id",…] maxlength="50"
neither → attrs=["class","id","aria-describedby","aria-invalid","type","name"] maxlength=null

This reproduces the filer's measurement exactly, so #5201's merge did not cover this branch. One declaration split into two outcomes by spelling: camelCase maxLength capped by coincidence (it happens to name a real DOM attribute), while the legacy max_length capped nothing at all and landed as a stray, inert max_length="50" attribute — invalid HTML that reads like a working cap to whoever greps this file next. Two independent defects.

After the change, all four rows read maxlength="50" / no attribute respectively, with no stray key:

max_length: 50 → attrs=["class","maxlength","id","aria-describedby","aria-invalid","type","name"] maxlength="50" max_length=null
both (50/80) → attrs=["class","maxlength","id",…] maxlength="50"
neither → attrs=["class","id","aria-describedby","aria-invalid","type","name"] maxlength=null

Why this is a fix, not a renderer-side tolerance

max_length is a live authoring spelling (AGENTS.md #0.1 does not bite here): the registered field:* widgets have dual-read maxLength ?? max_length since framework#1878 §3, all three producers of a form field normalize it (ObjectForm, sectionFields, EmbeddableForm.applyDefaultMaxLengths), and @object-ui/types declares it on several field types. This branch serves a hand-authored FormSchema handed straight to the renderer, where there is no normalizing producer in between and the author is the producer — so it was the one reader in the repo that dropped the declaration.

Sibling implementations mirrored

The fix shape is inherited from triage, not chosen here — a local destructure, copied from the two landed siblings in the same file:

⛔ The shared stripRendererOnlyProps table is deliberately untouched. It feeds checkbox, switch, select and this fallback alike, so widening it would silently change three branches this card neither fixes nor tests — the alternative triage explicitly rejected. The only occurrence of that helper's name in this diff is inside an explanatory comment.

Out of scope and unchanged: this branch still does not do the input arm's value={… ?? ''} normalization, and no {n}/{max} counter is added (the counter half remains the undecided design question #5201 left open).

Tests — both halves asserted

New file packages/components/src/renderers/form/__tests__/form-builtin-default-branch-max-length.test.tsx, copying #5201's criteria: every case reads getAttributeNames()andgetAttribute('maxlength'), because the missing cap and the stray attribute are two distinct defects and either assertion alone passes against half the bug. Six cases: a routing guard proving the default arm is really the path under test (counter-probed against a populated registry, so the two undefined lookups are a reading and not an empty registry), camelCase still capping, legacy max_length now capping, no stray key on the DOM, canonical winning when both are declared, and an uncapped field left byte-for-byte as it was.

Reverse verification

Predicted before running, with the pre-fix form.tsx restored and the new tests kept:

testpredictedobserved
routing guardgreengreen
camelCase capsgreengreen
legacy max_length capsredredexpected null to be '50'
no stray attributeredredexpected [ 'class', 'max_length', … ] to not include 'max_length'
canonical winsred on the 2nd assertion onlyred on the 2nd assertion onlyexpected [ 'class', 'maxlength', …(6) ] to not include 'max_length'
uncapped unchangedgreengreen

Tests 3 failed | 3 passed (6) — predicted and observed agree, including that the "canonical wins" case fails only on its stray-attribute half while its cap assertion was already green pre-fix. That is the concrete demonstration that a cap-only test would have let the invalid attribute survive.

No rebuild gates this ablation: the subject is reached through a relative source import (../../../renderers./form), not across a package exportsdist/ boundary, so the mutation reaches the tested code directly. Restoring afterwards left the working tree byte-identical to the committed fix (git status clean) and the suite back at 6/6 green.

Verification

All run from the repo root at commit 6ce9edfcd with a clean working tree:

  • pnpm exec vitest run packages/components/163 files, 1479 tests passed, covering the sibling input/textarea ceiling suites and the checkbox/switch/select arms that share the untouched strip table
  • pnpm --filter @object-ui/components type-check — clean (tsc --noEmit && tsc -p tsconfig.test.json, so the new test file is covered; the script name echoed, confirming it was not a zero-match no-op)
  • pnpm --filter @object-ui/components lint0 errors (891 pre-existing package-baseline warnings)
  • pnpm --workspace-concurrency=2 --filter '@object-ui/components^...' build — dependency closure builds clean
  • node scripts/check-control-bytes.mjs — OK (4664 files), plus a direct grep -naP over the three changed files
  • node scripts/check-changeset-presence.mjs — OK, 1 changeset declared
  • check:self-import, check:esm-specifiers, check:phantom-deps — all green

Changeset: patch on @object-ui/components (never major — 39 packages share one fixed group).


Generated by Claude Code

…back branch
The last arm of the built-in field switch — serving a `type` that is neither a
`BUILTIN_FIELD_TYPES` member nor resolvable from the registry — spread its props
onto the rendered `Input` and never read the declared ceiling, so one declaration
split into two outcomes by spelling. Measured on main after #5201 landed:
max_length: 50 -> attrs=["class","max_length",...] maxlength=null
maxLength: 50 -> attrs=["class","maxlength",...] maxlength="50"
camelCase capped by coincidence (it names a real DOM attribute); the legacy
`max_length` capped nothing and landed as a stray, inert attribute — invalid
HTML that reads like a working cap. Two independent defects.
Fixed with the same shape as the landed `input` (#5201) and `textarea` (#3439)
arms: a local destructure of the legacy key plus a locally resolved
`maxLength ?? max_length`, applied after the spread. The shared
`stripRendererOnlyProps` table is deliberately untouched — it feeds `checkbox`,
`switch` and `select` as well.
Fixes#5253
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)25.3 KB350 KB
Entry fileindex-BG_rSeu4.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.83KB3.70KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.33KB7.05KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.79KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.71KB1.22KB
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.02KB0.88KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.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)506.27KB113.31KB
core (index.js)4.11KB1.62KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.34KB
fields (index.js)237.07KB59.46KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.42KB1.39KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)29.43KB7.15KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)39.16KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.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.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.75KB18.37KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)128.04KB32.75KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)241.46KB60.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)123.77KB30.07KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)198.22KB53.27KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.66KB27.13KB
plugin-map (index.js)19.96KB6.56KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)42.84KB11.77KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.08KB20.59KB
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.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)31.56KB10.70KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
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)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.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-retry.js)4.32KB2.02KB
types (index.js)3.08KB1.53KB
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 (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

内建 form 的 default 回退分支有和 #5201 完全相同的 ceiling 缺陷:max_length 拿不到上限,只在 DOM 上留下失效属性

2 participants

@os-support-ai@claude