Skip to content

fix(app-shell,fields): valid package-sheet header markup; select field controlled from its first render (objectui#11804) - #11879

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11804-react-markup-warnings
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11804-react-markup-warnings

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11804
Clause-②: no

Both React warnings from the 2026-10-07 Studio QA pass are fixed at the component that emits them. Each emitting site was measured before any edit (happy-dom renders through the real components, with console.error / console.warn captured). Both PM hypotheses were partly wrong; the corrections are below.

What was measured first

The div inside a p (H1, corrected). The sheet's SheetDescription is not the cause: it holds only the package id as text. The cause is the Pending changes header in PackageDetailSheet (PackagesPage.tsx). It was a p holding the draft-count Badge, and the Badge primitive (packages/components/src/ui/badge.tsx, left untouched) renders a div. React logged "In HTML, div cannot be a descendant of p" (with its companion "p cannot contain a nested div") only for a package with pending drafts. The same sheet with no drafts logged nothing. Studio's Package info & settings menu item opens this same PackageDetailSheet, so console and Studio share the one site.

The Select flip (H2, corrected). The emitting widget is the single-value branch of SelectField (@object-ui/fields). It is registered as field:select, which is the form type the object create form gives every select field. The widget passed the form's undefined straight to Radix's Select for a field the record did not seed, so the Select started uncontrolled. The flip was not caused by defaults loading. A field seeded with a defaultValue was controlled from its first render. Opening the form logged nothing. The warning fired on the user's first pick in a select field that has no default. Measured through ObjectForm in create mode, on an account-shaped object with industry (no default) and status (defaultValue: 'prospect'):

  • mount: no warning;
  • picking industry: "Select is changing from uncontrolled to controlled";
  • payload: {"status":"prospect","name":"Acme","industry":"finance"}.

Other selects with the same shape, as read on this branch:

  • RecordPickerDialog's filter select and GridField's cell select already pass ''.
  • The inline-edit host InlineFieldInput passes '', and ActionParamDialog passes ?? null. SelectField already maps null to ''.
  • FieldEditWidget (the grid's inline editor) passes the raw value. It goes through SelectField, so this fix covers it.
  • The form renderer's built-in select branch (BuiltinSelectControl in form.tsx) is reached only by a bare type: 'select' form field, never by the object create form. It flips the same way on a first pick (measured). It is left alone here, see Acceptance notes.

The fix

  • PackagesPage.tsx: the header p becomes a div with the same classes. Tailwind's preflight zeroes a p's margins, so the spacing does not change.
  • SelectField.tsx: a host that hands the widget an onChange gets a controlled Select from the first render. undefined now maps to '', as null already did. Radix renders '' and undefined the same way (the placeholder, and an empty hidden native select), so nothing changes on screen. The saved value does not change either. Only the control's value changes: the host's own state still holds undefined, onValueChange is still the host's onChange, and the cascade clear and the read-only display still read the raw value.
  • A host with no onChange still gets undefined and stays uncontrolled. That host is a bare SDUI field:select node: SchemaRenderer hands it neither a value nor an onChange. If its value were mapped to '', the control would freeze on the placeholder after a pick, because nothing would ever hand the pick back. The comment this change replaces named exactly that risk, and the guard keeps it out.
  • .changeset/11804-react-markup-warnings.md: a patch for @object-ui/app-shell and @object-ui/fields.

No export, prop, @object-ui/types member, accepted input or packages/i18n key changes.

Pins

  • PackagesPage.pendingHeaderMarkup-11804.test.tsx uses the real PackageDetailSheet with two drafts. It checks that the header and its count badge render, that React logs no nesting error, and that no p in the document holds a block element. With no drafts, there is no header and no error.
  • SelectField.controlledFromFirstRender-11804.test.tsx has three hosts:
    1. The real form renderer hosting two unseeded field:select fields. Picking one logs no flip and the trigger shows the pick. On submit, the picked field is 'silver' and the untouched one is undefined, never ''.
    2. A bare widget with an onChange. Going from undefined to a value logs no flip. The empty trigger still shows the placeholder, and mounting does not call onChange.
    3. A host with no onChange. The control stays uncontrolled and still shows the pick.

Reverse check (run on committed 0fefd45, all mutation legs restored and proven)

Each mutation was applied by ablation-replace.mjs: the anchor hit once and the blob hash changed. After every leg the blob hash matched HEAD again and git diff HEAD was empty.

Mutation Expected Observed
M1: header back to a p the drafts case goes red, the no-drafts case stays green 1 failed / 1 passed. The failure lists React's "cannot be a descendant of" and "cannot contain a nested" errors
M2: delete the undefined branch (the pre-fix behaviour) hosts 1 and 2 go red, host 3 stays green 2 failed / 1 passed. Both failures show "Select is changing from uncontrolled to controlled"
M3: map undefined to '' unconditionally (no onChange guard) host 3 goes red, hosts 1 and 2 stay green 1 failed / 2 passed. The trigger stays on the placeholder after the pick

The first M2 attempt was a no-op and is void. Its replacement text was a substring of the anchor, so ablation-replace refused the leg and ran nothing. It was re-run as a delete leg, with the result shown above.

Gates (local, on 0fefd45; origin/main cef0eee merged in as 1e4bf56)

  • pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build exited 0. That is 29 packages, and it covers the @object-ui/fields^... closure.
  • pnpm --filter @object-ui/fields type-check and pnpm --filter @object-ui/app-shell type-check exited 0. Both chain tsconfig.test.json, and --listFilesOnly shows each one includes its new pin.
  • pnpm exec vitest run --maxWorkers=2 packages/fields/ plus 61 other test files exited 0: 302 files passed, 1 skipped; 4780 tests passed, 7 skipped. The 61 files are every test that names SelectField, select-trigger- or field:select, plus every PackagesPage test.
  • pnpm check:control-bytes, check:new-line-citations, check:changeset-claims, check:pending-changeset-literals, check:test-path-roots, check:unreferenced-sources and check:vi-mock-specifiers all exited 0. So did node scripts/check-changeset-presence.mjs and node scripts/check-changeset-no-major.mjs.
  • eslint --no-inline-config on the four touched .tsx files gave 0 errors (warnings only, the repo's usual no-explicit-any / hook-dependency shapes). This is a narrowed run. The repo-wide pnpm lint belongs to CI.

Acceptance notes

  • Same pattern, not fixed here: the form renderer's built-in select branch (BuiltinSelectControl, packages/components/src/renderers/form/form.tsx) passes toControlValue(value), which is undefined for an unseeded field. A first pick on a bare type: 'select' form field logs the same flip (measured in happy-dom through the real form renderer). The fix would be the same shape, ?? '' at that call site and not in the shared toControlValue, which the SDUI select renderer and radio-group read as "absent" for their defaultValue. It is left out because it is not the ruled site, the object create form never reaches it, and it would add @object-ui/components to this change's verification surface. No public door or real producer was measured for it, so it is noted here rather than filed.
  • StudioDesignSurface.tsx was only read. Nothing under packages/components/src/ui/** changed.

Generated by Claude Code

claude added 3 commits October 8, 2026 03:02
…rolled from first render

The package sheet's "Pending changes" header was a <p> holding the
draft-count Badge, which renders a <div>; React logged a DOM-nesting
error whenever a package with drafts opened the sheet. The header is
now a div with the same classes.

SelectField handed Radix an undefined value for a field the create
form had not seeded, so the user's first pick flipped the Select from
uncontrolled to controlled. A host that hands the widget an onChange
now gets a controlled Select from the first render ('' = nothing
chosen); a host with no onChange stays uncontrolled. Saved values are
unchanged.

Claude-Session: https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z
Co-authored-by: Claude <noreply@anthropic.com>
… package-sheet pin

So a regression's first failure shows React's own nesting error.

Claude-Session: https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 333 chunks) 3347.5 KB 3363.9 KB
Main entry chunk (gzip) 86.0 KB 350 KB
Entry file index-2rwTqmwi.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 584.59KB 140.69KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 238.60KB 66.51KB
fields (index.js) 266.92KB 67.47KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 38.37KB 10.31KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.50KB 11.82KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.39KB 15.52KB
plugin-charts (index.js) 84.71KB 23.25KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.82KB 39.17KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 248.57KB 65.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 176.62KB 45.75KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 239.75KB 65.92KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 117.42KB 29.32KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.10KB 11.81KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.07KB 22.93KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

Sign up for free to 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.

console: React warnings in Studio — <div> inside <p> in the package sheet, Select switching uncontrolled→controlled in the create form

2 participants