…y instead of blanking it silently (objectui#10567)
`toDateInputValue` keeps a stored `2026-02-30` verbatim, but an
`<input type="date">` sanitises a nonexistent day to an empty value, so
`DateField` and the sub-grid's editable `date` cell showed an empty
control with no marker. Both now judge the day as written with
`isImpossibleStoredDay`, hand the control an empty value, set
`aria-invalid`, and name the stored string in a notice linked through
`aria-describedby` (new date-only key `fields.date.impossibleDay` in all
ten packs plus `FIELD_DEFAULTS`). The `de` quote-pairing ratchet moves
64 to 65 for the new German value.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
Fixes #10567
Clause-②: no
What changed
This is the date-only half of objectui#10474 (PR objectui#10566), with the same face.
DateFieldand the sub-grid's editabledatecell inGridFieldnow check a stored value withisImpossibleStoredDay, which objectui#10566 added topackages/fields/src/widgets/nativeDateValue.ts. There is no second predicate, andnativeDateValue.tsis not edited.aria-invalid, and show a notice that names the stored string. The control'saria-describedbypoints at the notice, and a host's ownaria-describedbyis kept in front of the notice id. Examples:2026-02-30,2024-02-31,2025-02-29, and the ISO spelling2026-02-30T00:00:00.000Zthat an API may hand back.aria-describedby, andaria-invalidis unchanged (falseonDateField, absent on the grid cell).fields.date.impossibleDay, in all ten packs plusFIELD_DEFAULTS:The stored value "VALUE" is not a real date. Pick a date to replace it.The existingfields.dateTime.impossibleDayasks the user to pick "a date and time", which does not fit a date-only control, so it is not reused. The grid cell picks the key by column type.dequote-pairing ratchet goes from 64 to 65 for the new German value (one more matched „…“ span), with a rationale line, the same way objectui#10566 moved it.toDateInputValueis unchanged. It still returns the leading day verbatim, which the objectui#10474 pin keeps. The widget hands the control the empty value, not the adapter.Why the control is handed an empty value explicitly
The browser would paint it empty anyway. Handing it the empty value keeps the markup honest. Before this change the rendered
valueattribute held2026-02-30while the control read back empty. That attribute-versus-property split is the onenativeDateValue.tsdocuments for objectui#3127. It also matches the date-time face, whose adapter already hands the control an empty value.Measurements (the dispatch's mechanism assumptions)
executablePath/opt/pw-browsers/chromium), re-run by this dev because the card relayed it unrun. Aninput type=dateset to2026-02-30or2025-02-29reads back empty, while2026-02-28and2024-02-29read back as set. A control whosevalueattribute is2026-02-30reads back empty withvalidity.validtrue, so the browser raises no invalid state by itself.domtest project's environment) sanitises the same way on the property path: set to2026-02-30, it reads back empty, while the attribute keeps the string. On the unfixed tree the pin's empty-control assertion held and the first red was the missing marker. So the pin asserts what the component renders (the marker, the notice and its link), and the empty control holds in both environments.DateFieldrenders the sharedEmptyValueaffordance for an impossible day, byte-identical to what it renders fornot-a-date. That is objectui#10026 ruling A's marker, pinned bydate-carriers.impossibleDay-10026.test.tsx(green in this run). It neither rolls the day nor shows the stored string. The grid's read-only date text shows the raw stored string (objectui#3569). Neither face changes here.Pins (
DateField.impossibleDay-10567.test.tsx, 18 tests)DateField, four impossible spellings: the control is empty,aria-invalidistrue, and the notice names the stored string and is linked througharia-describedby. No rolled day appears anywhere, and a blur with no edit writes nothing. A hostaria-describedbyis kept. Picking2026-02-28writes it and clears the marker and the notice.datecell: the same four spellings and the same checks. Picking a real day writes it and clears the marker and the notice.2026-02-28and2024-02-29(a leap day), and empty values (empty string, undefined, an empty cell), give no marker, no notice and noaria-describedby.Directions predicted before the runs: the unfixed tree gives 11 red and 7 green. The ablation gives the same, with the objectui#10474 pin fully green.
21d34d5e2): 11 failed, 7 passed. Every failure is a marker assertion.b78821cd6. objectstack'sscripts/ablation-replace.mjsturnedconst impossible = isImpossibleStoredDay(value);intoconst impossible = false;inDateField.tsx. It also dropped thedatearm of the grid's check ((c.type === 'date' || c.type === 'datetime') && ...becamec.type === 'datetime' && ...). That restores the pre-card behaviour of both faces. On disk, the anchors went from 1 to 0, the replacements from 0 to 1, and both blobs changed. Result: 11 failed, 29 passed. Every impossible-day row went red, the 7 controls stayed green, and the 22 tests ofDateTimeField.impossibleDay-10474.test.tsxstayed green. Both files were restored: each blob equals HEAD's andgit diff HEADis empty. No build was involved, because the pin imports the widgets by relative path.Gates (head
b78821cd6)pnpm exec vitest run packages/fields/ packages/i18n/from the repo root: 275 files passed, 1 skipped; 4543 tests passed, 7 skipped.@object-ui/fieldsdependency closure (pnpm --filter '@object-ui/fields^...' build, exit 0), then rantype-checkfor@object-ui/fieldsand@object-ui/i18n: bothDone, exit 0.tsc -p tsconfig.test.json --listFilesincludes the new pin.check:control-bytes,check:new-line-citations(0 new),check-changeset-presence,check:changeset-claims,changeset:check,check:i18n-keys,check:i18n-drift,check:i18n-dead-keys(the new key is not listed),check:pending-changeset-literals,check:test-path-roots,check:self-import,check:phantom-deps. My own control-byte scan over the changed files found 0 hits.check:changeset-claimsnames three pending changesets that mention a touched file: 6625 (GridField.tsx), 6661 (en.ts) and 7122 (useFieldTranslation.ts). I read all three paragraphs. None is about the lines this PR changes, and all three still hold.--no-inline-configover the 15 changed.ts/.tsxfiles. The file count comes from eslint's own JSON output: 15 at head, 14 at base because the pin is new. Errors are 0 at base and at head. Per rule, base vs head:no-explicit-anygoes from 16 to 18 (both in the new pin: the emptyfieldfixture and the grid column fixture), and every other rule is unchanged. The config enables no type-aware linting (noparserOptions.project), so this diff cannot change the verdict on any untouched file.check:eager-locale-catalogues, reason: PREREQUISITE NOT MET (it reads a built console bundle). Left to CI.Acceptance notes
@object-ui/plugin-detailInlineFieldInput: its native date branch serves bothdateanddatetimefields and slices the stored value to its leading day. Probe (happy-dom render, not committed): a stored2026-02-30, or2026-02-30T10:00:00Zon adatetimefield, gives aninput type=datewhose value reads back empty while the attribute holds2026-02-30, with noaria-invalidand no notice. That is this card's silent blank.@object-ui/componentsdata-table inline cell editor (data-table.tsx) keeps its own private copies of the two adapters. Itsdatetimecopy goes straight to the engine, so2026-02-30T10:00:00Zshows as2026-03-02T10:00in UTC (engine probe of the same expression), which is the objectui#10474 roll. Itsdatecopy has this card's silent blank. Read at source, not rendered.Implemented by the
domain:ui#1seat, sessionsession_01BA3nKVUwKQJf8DBxrSVtNC(dispatched subagent).Generated by Claude Code