Přeskočit na obsah

datetime-local parsovaný přes new Date() — v Safari padá validace budoucího času

import { Aside } from ‘@astrojs/starlight/components’;

Uživatel v plánovači odeslání (e-mail, notifikace…) vybere čas pár minut v budoucnosti a klikne na potvrzení. UI odpoví „Vyberte čas v budoucnu”, přestože čas v budoucnu prokazatelně je. Reprodukovatelné jen v některých prohlížečích (hlášeno ze Safari na macOS); v Chrome funguje.

// ŠPATNĚ — chování závislé na prohlížeči
if (!value || new Date(value) <= new Date()) {
toast.error("Vyberte čas v budoucnu.");
}

input[type=datetime-local] vrací "YYYY-MM-DDTHH:mm" — bez sekund a bez časové zóny. Parsování takového řetězce přes Date konstruktor není napříč prohlížeči definované stejně:

  • WebKit/Safari historicky vracel Invalid Date, jindy hodnotu interpretoval jako UTC nebo půlnoc daného dne (čas zahodil) — porovnání s new Date() pak selže nebo projde špatně,
  • při degradaci inputu na textové pole (starší prohlížeč, autofill) přijde rovnou lokalizovaný tvar "10.07.2026 01:25", který V8 zaparsuje jako 7. října (MM.DD), WebKit jako Invalid Date,
  • porovnání Invalid Date <= now je vždy false (NaN), takže chyba se projeví až dál v pipeline (toISOString() hodí RangeError) — nebo naopak validace tiše pustí špatné datum.

Druhotný problém: generická hláška („Vyberte čas v budoucnu”) zamaskovala skutečný důvod — nešlo poznat, jestli je vadné parsování, prázdná hodnota, nebo opravdu čas v minulosti.

Parsování vytáhnout do čisté, testovatelné funkce; Date skládat po složkách (komponentový konstruktor = vždy lokální čas) a round-trip kontrolou odmítnout neexistující datum (31. 2. apod.). Chybové hlášky nesou konkrétní důvod včetně toho, co se přečetlo a kolik je teď.

export function parseDatetimeLocal(value: string): Date | null {
const v = value.trim();
const iso = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?$/.exec(v);
if (iso) return buildLocalDate(+iso[1], +iso[2], +iso[3], +iso[4], +iso[5]);
// fallback pro degradovaný input: "DD.MM.YYYY HH:mm"
const cz = /^(\d{1,2})\.\s*(\d{1,2})\.\s*(\d{4})\s+(\d{1,2}):(\d{2})$/.exec(v);
if (cz) return buildLocalDate(+cz[3], +cz[2], +cz[1], +cz[4], +cz[5]);
return null;
}
function buildLocalDate(y: number, mo: number, d: number, h: number, mi: number): Date | null {
const date = new Date(y, mo - 1, d, h, mi, 0, 0);
const ok = date.getFullYear() === y && date.getMonth() === mo - 1 &&
date.getDate() === d && date.getHours() === h && date.getMinutes() === mi;
return ok ? date : null; // round-trip odmítne 31. 2., 25:00…
}

Validace pak vrací tři rozlišené stavy: prázdno („Vyberte datum a čas.”), nečitelný tvar (s citací vstupu) a čas v minulosti (se zadaným i aktuálním časem). Unit testy pokrývají kanonický tvar, sekundy, mezeru místo T, lokalizovaný fallback a neplatná data.

Jak to příště odhalit dřív

Sekce “Jak to příště odhalit dřív”
  • Unit test na parsovací funkci s přesným scénářem z bugreportu (čas +2 minuty od „teď”).
  • Chybové hlášky validace vždy s konkrétním důvodem a echem vstupu — generická hláška prodlouží diagnostiku o kolo komunikace s uživatelem.
  • Cross-browser smoke test formulářů s datetime-local (Safari se chová jinak než Chromium).
Přidal aiarchitekt.cz · 10. 7. 2026 2:00
Provozuje aiarchitekt.cz