Uh oh!
There was an error while loading. Please reload this page.
fix(fields): the last five inline widgets read the delivered error slot - #7141
Conversation
…slot
objectui#7008 made `FieldEditWidget` deliver the declared `error` key. Of the
27 distinct components in `EDIT_WIDGETS`, 21 read it; five did not, so for
their field types the delivery was inert and `aria-invalid` was never set:
`TextField`, `BooleanField` (`boolean` + `toggle`), `DateField`,
`DateTimeField`, `TimeField`.
Each now computes `aria-invalid={!!error}` AFTER its DOM pass-through spread —
one existing idiom (the objectui#3222 discipline the other 21 share), so a
valid field says an explicit "false" rather than staying mute. `BooleanField`
is the only composite: the mark goes on its Radix `Checkbox` / `Switch`
button, never the wrapping div (objectui#5223).
The form path was never broken (`<FormControl>`'s Radix Slot delivered
`aria-invalid` through each widget's spread) and is unchanged; the gap was
every host without that Slot — the kanban required-fields dialog, the grid's
inline cell editor, the detail page's inline edit.
Marking only: the message text stays with the host.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM✅ 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-warren
commented
Sep 1, 2026
✅ ACCEPT (on the substance) — PM seat ( |
os-warren
commented
Sep 1, 2026
✅ Reviewed and armed — PM as reviewer-of-recordUndrafted and auto-merge enabled (squash) at head CI read in full, in one callAll 30 check runs fetched in a single request with 27 success, 3 skipped, 0 failed, 0 pending. The three skipped are Pre-check at arm time: Clause ② — not engagedThis consumes the What earned the armThree things in this report are the reason it goes in without a round trip: The census was re-taken after its blocker landed, not reused. The card's population was measured pre-#7128. Re-deriving it at Every apparent zero was opened, not counted. The ablation asserts the passing count, not just the red. Also correct: The PM assumption that got falsified — goodI fenced the population at Same for the stale doc comment in #7128's dialog test: corrected in place with a pointer to where the One sub-claim I am recording as unconfirmed, not confirmed
Arming anyway, because that claim is not load-bearing: the independent argument stands on its own — this diff registers nothing, so a registration-pin gate has no subject in it regardless of whether CI executes the gate. Flagging it so the unconfirmed half is not later remembered as verified. Will confirm the enqueue positively from the queue ref and post a landing note on objectui#7126 once Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#7126
The last five widgets in
EDIT_WIDGETSnow read theerrorslot that objectui#7008 delivers, so an inlinetext/boolean/toggle/date/datetime/timecontrol that failed validation finally reportsaria-invalid.Leg 0 — the population, re-derived on
origin/mainAFTER its blocker landedThe census in the card was taken before PR #7128 landed. Re-taken here at
f08bcd9af(fix(fields): FieldEditWidget delivers the declared NON-DOM block, fromgit log origin/main), in a dedicated worktree, never the shared checkout.Word-boundary
errormatch over the 27 distinct components inEDIT_WIDGETS:NumberFieldreturns 5,LookupField11, so a zero is a reading and not a broken query.UserFieldis a FALSE zero: its whole body isreturn LookupField {...(props as any)} field={fieldProp}, so it deliverserrortransitively and has always marked. Independently corroborated in-repo bydata-table.tsx, which already records "26 of the 27 components inEDIT_WIDGETScalltoDomPropsdirectly;UserFielddelegates its whole props object toLookupField, which does".⇒ the population is exactly the five the card names —
TextField,BooleanField(boolean+toggle),DateField,DateTimeField,TimeField. Nothing shrank, nothing grew.What changed
One idiom, five widgets — the objectui#3222 shape the other 21 already share (
SelectField,EmailField,NumberField): destructureerror, then writearia-invalid={!!error}after the DOM pass-through spread so the widget's own computation wins and a valid field says an explicit"false"rather than staying mute. Seven call sites, because two widgets have two render branches each.⛔ This buys the a11y MARKING only. The objectui#3222 slot drives
aria-invalidand renders no text; the visible message stays with the host (FormMessagein the form, the red hint in the kanban dialog). Nothing that was invisible becomes visible, and that is the defect being closed.The composite judgement, stated per widget
TextFieldinput[type=text]and thetextareaof therows > 1branchrowsconfigs silentBooleanFieldCheckbox/Switch, i.e.button[role=checkbox]/button[role=switch]divwith ansr-onlylabel. Thedivis deliberately NOT the target: a wrapper mark satisfies a subtree query while telling a screen-reader user nothing (objectui#5223).aria-invalidis a global ARIA attribute, valid on both roles, and both Radix roots forward unknown props to the real buttonDateFieldinput[type=date]DateTimeFieldinput[type=datetime-local]TimeFieldinput[type=time]None of the five turned out to lack a markable control.
The end-to-end case — the deliverable
packages/plugin-kanban/src/__tests__/RequiredFieldsDialog.ariaInvalidText-7126.test.tsx, following the shape of #7128'sRequiredFieldsDialog.ariaInvalid-7008.test.tsxrather than inventing a harness:textfield reportsaria-invalid="false"on a clean open and"true"after a submit attempt finds it empty — with the submit refusal and the visibleRequiredhint asserted first, so a dialog that silently submitted could not pass;textfilled and aselectempty, the select is"true"and the filled text is"false", and there is exactly oneRequiredhint — a dialog-wide flag would fail this;text,boolean,toggle,date,datetime,timein one dialog, each read off its own element selector, so a mark that landed onBooleanField's wrapper could not satisfy it.Plus a factory-level sweep,
packages/fields/src/__tests__/inline-widget-aria-invalid-7126.test.tsx: 8 cases (6 types + the two branch variants) x 2 directions, each requiring thearia-invalid="true"carrier to be focusable, plus two controls —select(a widget that already readerror, proving the harness measures a real delivery) anduser(pinning the census's FALSE zero so nobody later "fixes" a widget that was never broken).Why the factory and not the form. In the FORM these five were already announced correctly and always had been:
FormControlis a RadixSlotwhosearia-invalidreached each control through the props spread untouched, andwidget-aria-invalid-registry-e2e.test.tsxsweeps that path with an emptyNOT_YET_DELIVEREDledger. A form-based test would have been green before this change.FieldEditWidgetrenders no Slot, and it is the seam every non-form host composes — so that is the path measured.Ablation — the pins fail without the change
Direction predicted before the run and recorded in the script header. Mutation: delete all 7
aria-invalid={!!error}lines.HEADblob (e.g.TextField.tsxa9a17e42f->b919aa64d,BooleanField.tsxc8325fff6->d2adecc4c,TimeField.tsx239728047->762385d77). No rebuild leg is claimed and none is owed:vitest.config.mtsaliases@object-ui/fieldstopackages/fields/src, so the mutated source is what both pin files resolve — there is nodiston this path.Tests 19 failed | 2 passed (21).Tests 19 failed | 2 passed (21)— and the 2 that stayed green are exactly the two CONTROL cases (select,user), which do not depend on the five widgets. The passing count is asserted, not just the red: a suite that had collapsed to "no tests" would not have left 2 passers standing.git checkout HEAD -- ABSOLUTE_PATH(never a baregit checkout --, which reads the index a path-scoped checkout also writes): all five blobs back to theirHEADvalues, marker counts back to 2/2/1/1/1, andgit diff HEAD,git diff --cachedandgit status --shortall empty.Measurements
All at
323f2c381, the head of this branch.pnpm exec vitest run packages/fields/ packages/plugin-kanban/ --maxWorkers=2Test Files 151 passed (151)/Tests 2249 passed (2249)— includes the form-path registry sweep and #7128's own dialog pinpnpm exec vitest run(the 2 components-side negative-assertion files)Test Files 2 passed (2)/Tests 24 passed (24)pnpm --filter @object-ui/fields --filter @object-ui/plugin-kanban run type-checkScope: 2 of 47 workspace projects, bothDone. Itstsconfig.test.jsonleg provably covers the new tests —tsc --listFileslists both new files, so this is not the "typecheck excludes*.test.tsx" non-readingpnpm --filter @object-ui/fields --filter @object-ui/plugin-kanban run lint0 errors(warnings are pre-existing repo-wide; no--max-warningsin the gate). Plaineslint .per package, no--no-inline-configpnpm check:control-bytescheck-control-bytes: OK (scanned 5914 tracked text file(s); skipped 85 binary)— plus a self-scan of the 9 changed files with a probe control that hitspnpm check:vi-mock-specifiers/check:vi-mock-inherit/check:i18n-keyspnpm changeset:checkAll workspace packages are in the changeset fixed group./No changeset declares a major bump.node scripts/check-changeset-presence.mjs8 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)node scripts/check-changeset-overwrite.mjsNo pre-existing changeset was modified or deleted.pnpm check:sdui-registration-pinsPREREQUISITE NOT METtext: "the registrations this gate pins are dropped by a WRONG ARRAY at BUNDLE time, so a run with nothing to read has measured nothing. Build the console first". This change registers nothing; CI builds the console and runs ittype-checkfirst came back with a wall ofTS2307: Cannot find module '@object-ui/components'across files this branch never touched — an unbuilt closure, i.e. NOT MEASURED.pnpm --workspace-concurrency=2 --filter '@object-ui/fields^...' --filter '@object-ui/plugin-kanban^...' build(exit 0) fixed it, and the verdict above is from the re-run.PM assumptions, checked
EDIT_WIDGETSis the right population boundary."Partially falsified in structure, not in outcome — reported, not silently widened.InlineFieldInput(@object-ui/plugin-detail) renders four widgets that are NOT inEDIT_WIDGETSonto the same inline surface —ImageField,AvatarField,SignatureField,FileField— and already passeserror={error}to each. Measured with the same instrument and the sameNumberField-returns-5 control:ImageField4,AvatarField4,FileField10 all read it and mark;SignatureFieldreturns 0, and that zero is a documented verdict ("aria-invalidandaria-requiredare NOT here, and their absence is a verdict rather than an omission" — no keyboard path on the canvas), matching itsNOT_APPLICABLErow in the registry sweep. ⇒ no gap outside the boundary, nothing to file, and this PR stays at five widgets.NOT_YET_DELIVEREDledger is empty, and it exercises the form path where these five always marked via the Slot. One doc comment did: fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128's dialog test explained its choice ofselectwith "NOT every inline widget readserror—TextField,BooleanField,DateField,DateTimeFieldandTimeFielddo not". That sentence is now false, so it is corrected in place with a note saying what changed and where thetextcase now lives — not deleted, because it is the reasonselectwas chosen.Fences honoured
toDomProps.ts,toHostProps.tsandFieldEditWidget.tsxare untouched (git diff --statnames none of them) — that is #7008's landed work and its three compile-time assertions partition the contract. Nocontent/docs/releases/edit. Sibling lanes (#7121views/studio-design/, #7130packages/plugin-charts) were not entered; no collision seen.Generated by Claude Code