datetime-local parsovaný přes new Date() — v Safari padá validace budoucího času
import { Aside } from ‘@astrojs/starlight/components’;
Symptom
Sekce “Symptom”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.
Root cause
Sekce “Root cause”// ŠPATNĚ — chování závislé na prohlížečiif (!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í snew 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 jakoInvalid Date, - porovnání
Invalid Date <= nowje vždyfalse(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.
Fix
Sekce “Fix”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).