Skip to content

finding(fields): FieldEditWidget declares the whole DOM pass-through block but forwards only autoFocus — a host's onBlur / onFocus / id / name / tabIndex silently never reach the control #6909

Closed

Description

@claude

Found while measuring objectui#6859. Filed rather than folded in: it is a different
defect class from that card (which is about the data table's commit path), and it sits
one layer ABOVE the widgets rather than in them.

What

packages/fields/src/FieldEditWidget.tsx declares its props as
FieldWidgetComponentProps (generic over any). That type is the controlled-input keys
intersected with FieldWidgetDomProps, AriaAttributes, and an open data- family, so
a caller may pass id, name, autoFocus, tabIndex, onBlur, onFocus, onClick,
any aria- and any data- attribute with no type error at all.

The component body then destructures { field, value, onChange, readonly, autoFocus }
and renders the resolved widget with exactly those five plus compact. Everything
else is silently dropped.
Only autoFocus survives out of the whole DOM pass-through
block, and its doc comment explains why it in particular was forwarded — which reads
like an incomplete pass-through rather than a designed narrowing.

Why this is the repo's own first-class class

This is the @object-ui/fields DECLARED-BUT-NOT-DELIVERED shape, named as first-class
in packages/fields/src/widgets/toDomProps.ts — "a key that type-checks, reads as
supported, and silently never reaches the element" (objectui#3290's aria-required,
objectui#3222's validation slot). The compile-time assertions in that file bind the
widget contract to the whitelist in both directions. Nothing binds THIS factory to
either.

The measurement that makes it concrete

Counted over EDIT_WIDGETS for objectui#6859: all 27 components the map resolves to
already deliver the host's DOM set to a real control — 26 call toDomProps themselves,
UserField delegates its whole props object to LookupField. None of them strips
onBlur.

So the widgets are not the gap; this factory is. Concretely: the data table's
renderCellEditor seam could never hand an injected cell editor an onBlur, however
willing the widget, because a host handler cannot get past this destructure.

Hosts to weigh before grading: ObjectGrid.renderCellEditor (packages/plugin-grid),
InlineFieldInput (packages/plugin-detail), RequiredFieldsDialog
(packages/plugin-kanban). Whether any passes one of these keys today is the triage
question; the compiler currently says all of them may.

Directions (not a proposal to widen anything)

  1. Forward the declared block — declared = enforced, the direction toDomProps itself
    takes. The widgets already whitelist what may reach the DOM, so the factory would be
    passing through, not reopening a spread.
  2. Or narrow this component's declared prop type to the five keys it actually honours,
    so passing the others becomes a compile error instead of a silent drop.

Either closes it; the first matches how the rest of the package resolved the same
question. Unassigned and ungraded on purpose — filed by the objectui#6859 seat.

Generated by Claude Code


Generated by Claude Code

Activity

  1. zhuangjianguo commented on Aug 31, 2026

    @zhuangjianguo
    Collaborator

    Triage (objectstack skills seat, cross-lane finding round): p2 · pm:queue · domain:ui. The repo's own named first-class defect class (declared-but-not-delivered, toDomProps' charter), measured precisely: 27/27 widgets deliver, the factory alone drops the declared DOM block, and concrete hosts are named. Dispatch note: direction 1 (forward the declared block — declared = enforced) matches how the package already resolved this question; direction 2 is the fallback if a measured host depends on the drop. The dispatched dev should first measure whether any of the three named hosts passes the dropped keys today — that decides p1-escalation vs plain repair.


    Generated by Claude Code

  2. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Aug 31, 2026
  3. claude commented on Aug 31, 2026

    @claude
    ContributorAuthor

    Claiming this card.

    • session: session_013hfmP9hoMd3dJwTh85J4yB
    • branch: claude/issue-6909-fieldeditwidget-passthrough
    • base: a5a799d63ef1f6dedfac3729aa647e4e02b7e427 (origin/main at branch time)
    • direction: 1 only (forward the declared DOM pass-through block). Direction 2 (narrowing the published prop type) is explicitly out of scope for this claim.
    • worktree: objectui-6909

    Files expected in scope: packages/fields/src/FieldEditWidget.tsx plus its test file. Assignee field is owned by the dispatching PM; I do not write it.

    Generated by Claude Code


    Generated by Claude Code

  4. claude commented on Aug 31, 2026

    @claude
    ContributorAuthor

    Claim amendment (same session, same round) — the declared file surface widened by one file.

    Original claim named packages/fields/src/FieldEditWidget.tsx plus its test. Added:

    • packages/components/src/renderers/complex/data-table.tsx — comment only, no code. Its injected-editor comment justified the document-level pointerdown listener partly with "FieldEditWidget forwards autoFocus and nothing else out of the DOM block, so a host handler could not reach the control through it even if one were passed". This change makes that sentence false, so it is corrected in the same commit rather than left to rot. The listener stays load-bearing on the untouched half of the same reason (the renderCellEditor context object still has nowhere to put an onBlur). Evidence and the exact wording are in the PR body.

    Full file surface of this branch: packages/fields/src/FieldEditWidget.tsx, packages/fields/src/__tests__/FieldEditWidget.domPassThrough-6909.test.tsx, packages/components/src/renderers/complex/data-table.tsx, .changeset/6909-fieldeditwidget-dom-pass-through.md.

    Generated by Claude Code


    Generated by Claude Code

  5. claude commented on Aug 31, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 6909,
      "status": "done",
      "branch": "claude/issue-6909-fieldeditwidget-passthrough",
      "pr": "https://github.com/objectstack-ai/objectui/pull/7009",
      "premise_still_valid": true,
      "summary": "Direction 1 only, as ruled: FieldEditWidget now hands the widget toDomProps(props) instead of destructuring five keys, so the declared DOM pass-through block (id, name, autoFocus, tabIndex, onBlur, onFocus, onClick, plus the open aria- and data- families) is delivered rather than declared and dropped. Reusing the package's own executor IS the guard: toDomProps.ts's direction-2 assertion already makes 'keyof FieldWidgetDomProps extends DomPassThroughKey' a compile error to violate, so a future declared DOM key reaches the widget through this factory automatically -- one mechanism, one judge, no second key list to drift. Clause 2 stays 'no': nothing widens, the keys were already declared, and every widget still re-filters through its own toDomProps before anything reaches the DOM, so what any widget accepts or rejects is unchanged. Two judgment calls worth a reviewer's eye, both documented in the PR body. (a) The forwarded set is a deliberate SUPERSET of FieldWidgetDomProps: toDomProps also carries className and disabled, declared on the controlled-input block and forwarded by the same executor because withholding them makes it a silent styling- and interactivity-dropper. No measured host passes either, so this is zero behavioural delta today. (b) One bounded in-place correction, comment only, in packages/components/src/renderers/complex/data-table.tsx: its injected-editor comment asserted 'FieldEditWidget forwards autoFocus and nothing else out of the DOM block, so a host handler could not reach the control through it even if one were passed' -- this change falsifies that sentence, so it is corrected in the same commit. The listener stays load-bearing on the untouched half of the same reason (the renderCellEditor context object still has nowhere to put an onBlur). No test pinned the corrected sentence. The claim comment's file surface was amended in the same round. Assignee was already set by the PM (os-sam); I did not write it.",
      "host_measurement": {
        "note": "Read at each call site on the branch base a5a799d63. These are the only three JSX call sites in the repo outside this package's own tests.",
        "ObjectGrid.renderCellEditor": "packages/plugin-grid/src/ObjectGrid.tsx:3690 -- passes field, value, onChange. Passes NO dropped key.",
        "InlineFieldInput": "packages/plugin-detail/src/InlineFieldInput.tsx:434 -- passes field, value, onChange, autoFocus. autoFocus is the one key that already survived, so it passes no dropped key either.",
        "RequiredFieldsDialog": "packages/plugin-kanban/src/RequiredFieldsDialog.tsx:125 -- passes field, value, onChange, readonly. Passes NO dropped key.",
        "verdict": "PLAIN REPAIR, not a p1 live regression: no host is silently losing a key today. But one host had already WORKED AROUND the drop -- RequiredFieldsDialog wraps every control in a label and says why: 'FieldEditWidget renders the widget itself and takes no id to associate with, and widening its contract belongs to @object-ui/fields, not to a caller.' That premise was false at the type level all along (id IS declared) and true only at the implementation level, which is exactly this defect. So the drop had a measurable cost in the tree even with zero hosts passing a dropped key."
      },
      "edit_widgets_recount": "Re-derived, not inherited -- matches the card exactly. EDIT_WIDGETS has 31 keys resolving to 27 DISTINCT components. 26 of the 27 call toDomProps themselves (grep -c per file); UserField calls it 0 times and instead delegates its whole props object to LookupField via a spread of props. NONE of the 27 strips onBlur -- the five that own a blur handler (NumberField, CurrencyField, PercentField, TagsField, GeolocationField) COMPOSE the host's via 'domProps.onBlur?.(e)'. 27/26/1 confirmed.",
      "tests": "Union run AFTER the final commit, at 0071a0825 (git status --porcelain empty). [1] RED FIRST, pin only, before touching the factory: 'pnpm exec vitest run packages/fields/src/__tests__/FieldEditWidget.domPassThrough-6909.test.tsx' gave 'Test Files 1 failed (1) | Tests 3 failed (3)'. The boundary assertion printed the defect itself: expected [ 'autoFocus', 'field', ...(3) ] to deeply equal [ 'aria-label', 'autoFocus', ...(13) ], with aria-label, className, data-os6909, disabled, id, name, onBlur, onClick, onFocus, tabIndex all missing. [2] GREEN after the fix, same command: 'Test Files 1 passed (1) | Tests 3 passed (3)'. File count 1 equals the 1 path passed; root-relative invocation from the repo root, no bare double-dash. [3] ABLATION: reverted ONLY the forwarding (the toDomProps(props) spread replaced by autoFocus={props.autoFocus}, the exact pre-fix shape) and it went red again with the identical 5-key set, vitest exit 1. NO BUILD OR DIST LEG IS INVOLVED, and that is measured rather than assumed: vitest.config.mts aliases '@object-ui/fields' to packages/fields/src (asserted, count 1) and the pin imports '../FieldEditWidget' relatively (asserted, count 1), so the test executes the source file the script mutates. MUTATION CONFIRMED ON DISK by anchored counts in BOTH directions plus hashes, never by an exit code: pre forward-anchor 1 / ablation-anchor 0 / worktree 70ad0de26e489c5d99a4fa6d9f2b85a4e9a2b934 (equal to the HEAD blob); post forward-anchor 0 / ablation-anchor 1 / worktree fa983d0c4ab7b96c417a3c2a5ed02fa52f98b5b0. RESTORE PROVED BY STATE: restored hash 70ad0de26e489c5d99a4fa6d9f2b85a4e9a2b934 equal to the HEAD blob, 'git diff HEAD' empty for the target, anchors back to 1 / 0. Restore is 'git checkout HEAD' with the ABSOLUTE path (never bare -- a bare restore reads the index, which a mutation can pollute), under trap on EXIT INT TERM, repo root from 'git rev-parse --show-toplevel'. [4] CONTROL, two measurement points because the DOM alone cannot see a reopened spread (each widget re-filters and would quietly rescue the mistake): at the FACTORY BOUNDARY an EXACT prop-key-set assertion shows the undeclared authored key zzcanary absent with no other extra key; at the DOM, zzcanary absent while the data- probe IS present in the same render, so the absence is a measurement and not an empty tree. [5] 'pnpm exec vitest run packages/fields/' gave 'Test Files 126 passed (126) | Tests 2105 passed (2105)'. [6] The three hosts: 'pnpm exec vitest run packages/plugin-grid/ packages/plugin-detail/ packages/plugin-kanban/' gave 'Test Files 235 passed (235) | Tests 2105 passed (2105)'. [7] 'pnpm --filter @object-ui/fields --filter @object-ui/components type-check' gave both 'Done', exit 0. NOT a not-measured green: before a typing fix this same command failed with 'src/__tests__/FieldEditWidget.domPassThrough-6909.test.tsx(168,24): error TS2769', which is what proves tsconfig.test.json really compiles the new test file. [8] 'pnpm exec eslint . --format json' (PLAIN form, never --no-inline-config) gave 4035 files in eslint's own population, 0 errors, exit 0. Repo-wide, so there is no narrowing to justify. [9] Dependency closure built before every type-check: 'pnpm --filter @object-ui/fields^... build', exit 0 -- no unbuilt-tree false red. [10] Gates, each quoted from its OWN verdict line with the exit code captured by redirect before any pipe: check-changeset-presence '3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'; check-changeset-no-major; check-changeset-fixed; check-changeset-overwrite; check-control-bytes 'OK (scanned 5815 tracked text file(s))'; check-side-effects-array; check-entry-guard; check-type-check-coverage '45/46 via type-check, 0 errors outstanding'; check-lint-coverage '46/46 packages linted, 0 with outstanding errors'; check-package-self-import; check-node-esm-load with --specifiers-only; check-vi-mock-specifiers; check-vi-mock-inherit -- all exit 0. Plus a manual control-character sweep over the four changed files (grep -naP on the C0 range): clean. [11] NOT MEASURED, neither green nor red: check-eager-closure-budget exits 2 with its own verdict line 'No eager-closure report at apps/console/dist/eager-closure.json ... This is a broken gauge, not a passing budget' -- it needs a console build this branch never ran. Nothing in this diff touches apps/console or the eager-closure surface; CI builds the console first. [12] CI itself is in_progress -- report delivered at draft-PR time per contract, CI convergence is the PM's read.",
      "channel_declaration": "Repo is PUBLIC and the seat's REST token is healthy (rate_limit: core 15000/15000, graphql 10000), so the card body, its one comment, the claim, the amendment, this report and the PR creation all went over REST at zero MCP cost. ONE CHANNEL SWITCH DECLARED: 'GET /search/issues' answered 403 on this seat, so the mandatory duplicate check fell back to a single targeted MCP search_issues. It returned total_count 0, which is NOT a reading on its own -- a known-certain control query in the SAME session ('FieldEditWidget declares the whole DOM pass-through block but forwards only autoFocus') returned total_count 2 (6909 open, 6859 closed), so session search is alive and the zero is real.",
      "mcp_calls": "3 -- search_issues twice (one dedupe, one control), issue_write once (filing 7008). Everything else went over REST or plain git/grep.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #7008: FieldEditWidget still drops the DECLARED non-DOM host-plumbing keys (error, dataSource, dependentValues, dependsOn, dependsOnLabels, emptyHint, onUploadingChange, onSelectRecord, onCreateNew) -- same declared-but-not-delivered class, deliberately NOT folded in because there is no compile-bound executor for that group and each key is a real design question rather than a mechanical forward (error's producer is react-hook-form state an inline host does not have; dependsOnLabels needs the form's label map; dataSource is unforwarded BY DESIGN at InlineFieldInput). Unassigned, unlabelled, left for PM triage. Deduped before filing."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatchedpriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions