Repository navigation
fix(fields,components): a date typed in a form that the picker cannot read is a field error that refuses submit - #27623
Conversation
…t refuses submit The date / datetime picker never writes text that names no date, so the form kept its old value and saved it as if nothing had been typed. The picker now reports such text through a new onInvalidInputChange widget prop, and the form renderer turns the report into the field's validation error, which refuses submit the way a missing required value does. Claude-Session: https://claude.ai/code/session_01Utp6CXAtdK85r4832Kydbf Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CXAtdK85r4832Kydbf Co-authored-by: Claude <noreply@anthropic.com>
… in an effect, not during render Claude-Session: https://claude.ai/code/session_01Utp6CXAtdK85r4832Kydbf Co-authored-by: Claude <noreply@anthropic.com>
…-t6m105 Claude-Session: https://claude.ai/code/session_01Utp6CXAtdK85r4832Kydbf Co-authored-by: Claude <noreply@anthropic.com>
…ations instead of casting to any Claude-Session: https://claude.ai/code/session_01Utp6CXAtdK85r4832Kydbf Co-authored-by: Claude <noreply@anthropic.com>
…l widget prop FieldWidgetComponentProps is exported from @object-ui/fields, so the optional onInvalidInputChange key widens its published surface. Claude-Session: https://claude.ai/code/session_01Utp6CXAtdK85r4832Kydbf Co-authored-by: Claude <noreply@anthropic.com>
|
domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC Contract reviewServed-tier: Face: only the ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC 席位核对ACCEPT · head
Generated by Claude Code |
domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC
Requested by Bill · project thread
Fixes #27483
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
ui/packages/components/src/renderers/form/form.tsx(新校验项 invalidInput 与回调)、ui/packages/fields/src/widgets/date-picker/DatePickerField.tsx(上报与撤回)、ui/packages/fields/src/widgets/types.ts(新属性的契约说明)。pnpm --dir ui exec vitest run packages/fields/src/__tests__/date-picker-invalid-text-blocks-submit-27483.test.tsx,预期 5 条全绿。What changes
The date / datetime picker never writes text that names no date. In a record form that meant the form value stayed what it was (empty on create, the old date on edit), the submit went through and the success toast fired. Per the triage ruling, the unparseable text is now marked invalid through the form's existing field validation, the way a missing required value is.
onInvalidInputChange(@object-ui/fields,FieldWidgetComponentProps): a new optional widget prop. A widget that parses typed text fires it with the message to show when the committed text names no value, and withnullonce the text is valid, cleared, or the widget unmounts. Its doc comment names its producer (the date picker's two faces) and its consumer (the form renderer). It is added totoHostProps'HOST_PLUMBING_KEYS, so the compile-time partition still holds andFieldEditWidgetdelivers it when a host passes it.DatePickerField/DateTimePickerField: report on change only (not on every render), withdraw on unmount, and report nothing while readonly. A widget given the callback leaves the message text to the form's FormMessage, as theerrorslot already does; a host that passes none (the grid's inline editor, the action-param and bulk-action dialogs) keeps the picker's own notice, byte for byte.aria-invalidis set either way.formrenderer (@object-ui/components): a per-fieldinvalidInputMessagesmap (a ref, besiderequiredMessages, read by the validator at call time) and a firstrules.validateentry,invalidInput, that refuses while a message is held. The callback stores the message and runstrigger(name), so the error shows when the user leaves the field, not only at submit; a withdrawal clears only aninvalidInputerror.stripRegisteredFieldPropsforwards the callback to thedate/datetimewidgets only, so no other widget spreads an unknown handler onto its DOM.timewidget is a nativeinput[type=time]and shares neither the parser nor the commit path, so it is untouched.Tests
ui/packages/fields/src/__tests__/date-picker-invalid-text-blocks-submit-27483.test.tsx(new, realformrenderer plus the production registration of the picker): typing13/45/2026and leaving the field shows the form message andaria-invalid, with no second notice; pressing Create is refused (the form scrolls to the field) andonSubmitis never called; correcting the text clears the error and saves2026-10-20; clearing it lets an optional empty date save; a valid typed date saves (control);datetimerefuses the same way.ui/packages/components/src/renderers/form/__tests__/form-invalid-input-refuses-submit-27483.test.tsx(new, probe widget): a report becomes the field message at once and refuses submit; a withdrawal clears it and the submit goes through; onlyfield:datereceives the callback,field:lookupdoes not.DatePickerField-12622.test.tsx(updated, the existing "not written, and says so" pin is kept as is): with a host callback the picker reports the message (containing the text and the format), draws no own notice, reportsnullafter a valid commit, on clear and on unmount;datetimereports too.Acceptance notes
FieldEditWidgetfor the grid and record-page inline edit,ActionParamDialog,BulkActionDialog) pass no callback today, so there the picker still shows its own notice and does not block the host's save. Not reproduced here and not filed; noted as the remaining surface of the same channel.fields.datePicker.invalidstring ("Not saved: ... is not a date. Type it as FORMAT, or pick one from the calendar."), so no i18n key was added.mainmet the record-currency change instripRegisteredFieldProps; both allow-list lines are kept.Verification
Head
81611c8a. Test readings atbc5108de(after the one merge ofmain);c4826848only types the two new test files' probe and registrations instead of casting toany(both packages'type-checkexit 0 and the four pin files 33/33 there), and81611c8aonly changes the changeset's declaration toClause-②: yes (widening):FieldWidgetComponentPropsis exported from@object-ui/fields, and the new key is optional, so no consumer breaks. The derived gates are re-run at81611c8a.Tests and type-check (local, under the verify lock)
pnpm --dir ui exec vitest run --maxWorkers=2 packages/fields/ packages/components/: Test Files 734 passed, 2 skipped; Tests 8683 passed, 31 skipped; VERDICT command-exit 0. (Before the merge: 731 files, 8654 tests, exit 0.)pnpm --filter @object-ui/fields type-checkexit 0;pnpm --filter @object-ui/components type-checkexit 0.ui/files: 0 errors, and no warning on the lines this PR adds.pnpm check:new-line-citations(ui): 0 new citations.Ablation (one per tested behaviour, each through scripts/ablation-replace.mjs, restored by blob)
invalidInputvalidator made to pass always: 4 tests red (the two refusal pins in the fields end-to-end file, both renderer pins that need the error), 20 green; restored to blobc3d119a7bf03.556b44a4b738.@object-ui/componentsto itssrc.Gates (dispatch-gates derivation, 34 families)
dispatch-gates --ran: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN; every recorded exit 0.pnpm check:type-check-debt(exit 3, queue timeout, then exit 0 under the lock) andpnpm --dir ui exec vitest run scripts/(one test timed out at 15 s beside the package run, then 4901 passed alone).node scripts/check-sdui-manifest.mjsand its--self-test: exit 0.Generated by Claude Code