feat(date): add DateTimeField and migrate datetime inputs

Add a dependency-free DateTimeField that paints the date part in the
user's date-order preference and the time part in the hour-cycle
preference over a hidden native datetime-local input, preserving the
local yyyy-MM-ddTHH:mm value contract. Migrate the seven DateTimeInput
callsites (measurement add/edit, mood add/edit, lab add/edit,
medication intake quick-add) so the chosen format is honoured there too,
and cover the field's value contract and per-preference rendering with a
component test.
This commit is contained in:
Marc-André Bombeck committed 2026-06-26 13:17:55 +02:00
1 parent 0816b3b0c0
commit 01b24c4d73
9 files changed
+394 -21

No files matched your search

@@ -0,0 +1,93 @@
/**
* `<DateTimeField>` contract (v1.21.0).
*
* Like `<DateField>`, the SSR-only convention (`renderToStaticMarkup`,
* `environment: "node"`) means the interactive picker path can't be driven
* here. What the static markup pins is the load-bearing contract:
*
* - the committed VALUE stays a local `yyyy-MM-ddTHH:mm` string on a hidden
* native datetime-local input, so this is a drop-in for `<DateTimeInput>`;
* - the visible overlay paints the date in the date-order preference and the
* time in the hour-cycle preference;
* - disabled / min / max / placeholder thread through;
* - height + target-size parity classes are present.
*
* Under SSR both preferences resolve AUTO (no `window`), so the overlay follows
* the locale: de → "31.12.2026 14:05" (24h), en → "12/31/2026 02:05 PM".
*/
import { describe, expect, it } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { I18nProvider } from "@/lib/i18n/context";
import { DateTimeField } from "../date-time-field";
function render(node: React.ReactNode, locale: "de" | "en" = "en"): string {
return renderToStaticMarkup(
<I18nProvider initialLocale={locale}>{node}</I18nProvider>,
);
}
describe("<DateTimeField>", () => {
it("keeps the local datetime value on a hidden native datetime-local input", () => {
const html = render(
<DateTimeField id="ts" name="ts" value="2026-12-31T14:05" />,
);
expect(html).toMatch(
/<input[^>]*type="datetime-local"[^>]*value="2026-12-31T14:05"/,
);
expect(html).toContain('name="ts"');
expect(html).toMatch(
/<input[^>]*type="datetime-local"[^>]*class="[^"]*sr-only/,
);
});
it("paints the AUTO-locale display (de → dd.MM.yyyy + 24h)", () => {
const html = render(<DateTimeField value="2026-12-31T14:05" />, "de");
expect(html).toContain('value="31.12.2026 14:05"');
});
it("paints the AUTO-locale display (en → MM/dd/yyyy + 12h)", () => {
const html = render(<DateTimeField value="2026-12-31T14:05" />, "en");
// en-US default hour cycle is AM/PM.
expect(html).toMatch(/value="12\/31\/2026 02:05\s?PM"/);
});
it("renders the supplied placeholder when empty", () => {
const html = render(<DateTimeField value="" placeholder="Pick a moment" />);
expect(html).toContain('placeholder="Pick a moment"');
});
it("derives a format-shaped placeholder when none is supplied (en)", () => {
const html = render(<DateTimeField value="" />, "en");
expect(html).toContain('placeholder="MM/DD/YYYY --:--"');
});
it("threads disabled, min and max onto the native input", () => {
const html = render(
<DateTimeField
value="2026-12-31T14:05"
disabled
min="2020-01-01T00:00"
max="2030-12-31T23:59"
/>,
);
expect(html).toMatch(
/<input[^>]*type="datetime-local"[^>]*min="2020-01-01T00:00"/,
);
expect(html).toMatch(
/<input[^>]*type="datetime-local"[^>]*max="2030-12-31T23:59"/,
);
expect(html).toMatch(/<input[^>]*type="datetime-local"[^>]*disabled/);
});
it("ships the WCAG target-size + height-parity classes", () => {
const html = render(<DateTimeField value="2026-12-31T14:05" />);
expect(html).toContain("min-h-11");
expect(html).toContain("sm:h-10");
});
it("exposes a labelled calendar picker affordance", () => {
const html = render(<DateTimeField value="2026-12-31T14:05" />);
expect(html).toContain('aria-label="Open date picker"');
});
});