diff --git a/.changeset/10625-inline-date-impossible-day.md b/.changeset/10625-inline-date-impossible-day.md
new file mode 100644
index 0000000000..a72c4bbda0
--- /dev/null
+++ b/.changeset/10625-inline-date-impossible-day.md
@@ -0,0 +1,5 @@
+---
+"@object-ui/plugin-detail": patch
+---
+
+The detail page's inline date editor no longer blanks a stored day that does not exist without saying so (objectui#10625). A `date` field holding `2026-02-30`, or a `datetime` field holding `2026-02-30T10:00:00Z`, used to open an empty date input with no marker and no notice. The inline editor now uses `@object-ui/fields`' `DateField` for `date` and `datetime` fields, so it behaves like the form editor: the control is empty, marked `aria-invalid`, and described by a notice that names the stored value. Nothing is written until the user picks a day. A real date opens and saves as before (the edit is re-emitted as a full ISO timestamp at local midnight).
diff --git a/.changeset/9786-detail-timestamps-display-locale.md b/.changeset/9786-detail-timestamps-display-locale.md
index 34659f4028..6256c09a60 100644
--- a/.changeset/9786-detail-timestamps-display-locale.md
+++ b/.changeset/9786-detail-timestamps-display-locale.md
@@ -60,13 +60,15 @@ variable that resolves to `undefined`, and the only way to reach
`RecordMetaFooter` — whose absolute face is a `TooltipContent` child that Radix
keeps out of the DOM until the tooltip opens.
A census pin reddens when a new bare, `undefined`- or hard-coded-tag call site
-is written; it masks comments before it counts, and it declares
-`InlineFieldInput`'s `en-CA` as an exemption with its reason — that call feeds
-``, whose value HTML defines as `YYYY-MM-DD`, so it is an ISO
+is written; it masks comments before it counts, and when it landed it declared
+`InlineFieldInput`'s `en-CA` as an exemption with its reason — that call fed
+``, whose value HTML defines as `YYYY-MM-DD`, so it was an ISO
formatter and not a display locale. It landed scoped to this package and, under
the ruling on this card, became the one repository-wide census in
`@object-ui/i18n` (`machineLocaleCensus-9909.test.ts`, objectui#9909), which
-carries that exemption forward.
+carried that exemption forward. objectui#10625 later removed the call:
+`InlineFieldInput`'s date editor now renders `@object-ui/fields`' `DateField`,
+and the exemption went with it.
⛔ Not repaired here: the same class outside this package. A mechanical scan of
the whole repository's non-test sources — comments masked, the same matcher the
diff --git a/packages/i18n/src/__tests__/machineLocaleCensus-9909.test.ts b/packages/i18n/src/__tests__/machineLocaleCensus-9909.test.ts
index 6fc2e2b89a..1ff933f929 100644
--- a/packages/i18n/src/__tests__/machineLocaleCensus-9909.test.ts
+++ b/packages/i18n/src/__tests__/machineLocaleCensus-9909.test.ts
@@ -284,22 +284,13 @@ const DECLARED: Exemption[] = [
reason:
'the total line of the same dev-server-only demo page source; the console build ships `index.html` alone.',
},
- {
- file: 'packages/plugin-detail/src/InlineFieldInput.tsx',
- expression: "d.toLocaleDateString('en-CA')",
- count: 1,
- verdict: 'deliberate fallback',
- reason:
- 'an ISO-8601 FORMATTER, not a display locale: `` takes `YYYY-MM-DD` and nothing else, so ' +
- 'putting this on the display locale would blank the native date picker.',
- },
{
file: 'packages/plugin-gantt/src/GanttView.tsx',
expression: "task.start.toLocaleDateString('en-CA')",
count: 2,
verdict: 'deliberate fallback',
reason:
- 'the same ISO-8601 formatter feeding the inline editor\'s `` value (double-click and ' +
+ 'an ISO-8601 formatter, not a display locale: it feeds the inline editor\'s `` value (double-click and ' +
'context-menu edit both seed it).',
},
{
diff --git a/packages/plugin-detail/src/InlineFieldInput.tsx b/packages/plugin-detail/src/InlineFieldInput.tsx
index 8acf5c3de7..0e5314f3af 100644
--- a/packages/plugin-detail/src/InlineFieldInput.tsx
+++ b/packages/plugin-detail/src/InlineFieldInput.tsx
@@ -13,6 +13,7 @@ import {
LookupField,
UserField,
NumberField,
+ DateField,
CurrencyField,
PercentField,
ImageField,
@@ -494,8 +495,8 @@ export const InlineFieldInput: React.FC = ({
// how a chip row renders — are `FieldEditWidget`'s to make, and it already
// makes them for the grid's inline cell editor; asking it here is the point of
// delegating rather than routing type by type. `date`/`datetime` are excluded
- // because they ARE routed (the native date input below) and a working path
- // does not get churned. Types the hosts gate out entirely (containers,
+ // because they ARE routed (to `DateField` below, objectui#10625) and a
+ // working path does not get churned. Types the hosts gate out entirely (containers,
// credentials, computed — #4228 / #3355) never arrive here at all, and none of
// them has a widget anyway, so this branch cannot re-open those.
//
@@ -515,48 +516,54 @@ export const InlineFieldInput: React.FC = ({
/>
);
}
- const inputType = isDate ? 'date' : 'text';
- // needs a YYYY-MM-DD string; raw ISO timestamps
- // ("2026-02-14T14:46:20.862Z") leave the picker blank. Slice down to the date
- // portion so existing values round-trip correctly.
+ if (isDate) {
+ // The routed date editor is `@object-ui/fields`' own `DateField`, for
+ // `date` AND `datetime` fields alike (objectui#10625). That is the widget
+ // whose answer to a stored day that does not exist (`2026-02-30`, or
+ // `2026-02-30T10:00:00Z` on a `datetime` field) is objectui#10026
+ // direction A: an `` can only paint such a day blank
+ // (the browser sanitises it to `""`), so the control is handed `""`,
+ // marked `aria-invalid`, and described by a notice NAMING the stored
+ // string (`isImpossibleStoredDay` + `fields.date.impossibleDay`,
+ // objectui#10567). The hand-rolled input this replaces sliced the stored
+ // value into the control and nothing else, which is the silent blank.
+ //
+ // The read side is `toDateInputValue`: a leading `YYYY-MM-DD` passes
+ // through verbatim (an ISO timestamp is sliced to its date, as before),
+ // anything else goes through local calendar getters. The write side stays
+ // this editor's own: the widget hands back the control's `YYYY-MM-DD`,
+ // re-emitted as full ISO at local midnight so backend validation that
+ // expects ISO timestamps keeps working. Only a user edit emits.
+ return (
+ onChange(v ? new Date(v + 'T00:00:00').toISOString() : v)}
+ autoFocus={autoFocus}
+ error={error}
+ />
+ );
+ }
+ // Only the raw text fallback remains below.
+ // Coerce objects (e.g. an unexpanded reference that slipped through type
+ // detection) to a readable label rather than leaking "[object Object]".
const inputValue = value == null
? ''
- : isDate
- ? (() => {
- const s = String(value);
- if (/^\d{4}-\d{2}-\d{2}/.test(s)) return s.slice(0, 10);
- const d = new Date(s);
- return isNaN(d.getTime()) ? '' : d.toLocaleDateString('en-CA');
- })()
- // Coerce objects (e.g. an unexpanded reference that slipped through type
- // detection) to a readable label rather than leaking "[object Object]".
- : typeof value === 'object'
- ? String(coerceToSafeValue(value) ?? '')
- : String(value);
+ : typeof value === 'object'
+ ? String(coerceToSafeValue(value) ?? '')
+ : String(value);
return (
{
- const v = e.target.value;
- // Re-emit dates as full ISO so backend validation that expects ISO
- // timestamps keeps working.
- if (isDate && v) {
- const iso = new Date(v + 'T00:00:00').toISOString();
- onChange(iso);
- } else {
- onChange(v);
- }
- }}
+ onChange={(e) => onChange(e.target.value)}
/>
);
};
diff --git a/packages/plugin-detail/src/__tests__/InlineFieldInput.impossibleDay-10625.test.tsx b/packages/plugin-detail/src/__tests__/InlineFieldInput.impossibleDay-10625.test.tsx
new file mode 100644
index 0000000000..06df1cbd48
--- /dev/null
+++ b/packages/plugin-detail/src/__tests__/InlineFieldInput.impossibleDay-10625.test.tsx
@@ -0,0 +1,78 @@
+/**
+ * ObjectUI
+ * Copyright (c) 2024-present ObjectStack Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+/**
+ * The inline date editor and a stored day that does not exist (objectui#10625).
+ *
+ * `InlineFieldInput` edits `date` AND `datetime` fields with one native
+ * ``. A stored `2026-02-30` (or `2026-02-30T10:00:00Z` on a
+ * `datetime` field) is a value that control can only paint blank — the
+ * browser sanitises a nonexistent day to `""` — so on its own it is the silent
+ * blank objectui#10026 direction A rules out. The editor must answer it the way
+ * `@object-ui/fields`' `DateField` does since objectui#10567: an empty control,
+ * marked `aria-invalid`, described by a notice that NAMES the stored string,
+ * and nothing written until the user picks a day.
+ *
+ * Provider-free, like the other suites here: the notice falls back to the
+ * English default the fields package carries for provider-less rendering.
+ */
+
+import { describe, it, expect, vi } from 'vitest';
+import { render } from '@testing-library/react';
+import { InlineFieldInput } from '../InlineFieldInput';
+
+function renderInline(type: string, value: unknown) {
+ const onChange = vi.fn();
+ const { container } = render(
+ ,
+ );
+ const input = container.querySelector('input[type="date"]') as HTMLInputElement | null;
+ return { container, input, onChange };
+}
+
+describe('InlineFieldInput marks a stored impossible day (objectui#10625)', () => {
+ const cases: Array<[string, string]> = [
+ ['date', '2026-02-30'],
+ ['datetime', '2026-02-30T10:00:00Z'],
+ ];
+
+ for (const [type, stored] of cases) {
+ it(`a \`${type}\` field holding ${stored} shows an empty, invalid control described by a notice naming it`, () => {
+ const { container, input, onChange } = renderInline(type, stored);
+ expect(input).not.toBeNull();
+ expect(input!.value).toBe('');
+ expect(input!.getAttribute('aria-invalid')).toBe('true');
+
+ const describedBy = input!.getAttribute('aria-describedby');
+ expect(describedBy).toBeTruthy();
+ const notice = describedBy!
+ .split(' ')
+ .map((id) => container.ownerDocument.getElementById(id))
+ .find((el) => el != null);
+ expect(notice).toBeTruthy();
+ expect(notice!.textContent).toContain(`"${stored}"`);
+
+ // Nothing is written on mount: only a user edit emits.
+ expect(onChange).not.toHaveBeenCalled();
+ });
+ }
+
+ for (const [type, stored, shown] of [
+ ['date', '2026-02-14', '2026-02-14'],
+ ['datetime', '2026-02-14T14:46:20.862Z', '2026-02-14'],
+ ] as const) {
+ it(`a real day on a \`${type}\` field is unchanged: shown, not marked, no notice, no emit`, () => {
+ const { input, onChange } = renderInline(type, stored);
+ expect(input).not.toBeNull();
+ expect(input!.value).toBe(shown);
+ expect(input!.getAttribute('aria-invalid')).not.toBe('true');
+ expect(input!.getAttribute('aria-describedby')).toBeNull();
+ expect(onChange).not.toHaveBeenCalled();
+ });
+ }
+});