From 50db2fcb4b69696984eda30a9e254bc0850bd9b2 Mon Sep 17 00:00:00 2001 From: Marco Date: Thu, 23 Jul 2026 19:54:14 +0000 Subject: [PATCH] Refocus a checkout field automatically when its blur validation fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generalizes what the USt-IdNr. field already did for an unconfirmed VIES result — every blur-validated field now gets focus put right back on it the moment its own validation fails, instead of letting focus move on to wherever the customer tabbed/clicked next. The correction happens immediately rather than being left for a submit attempt to catch later. Co-Authored-By: Claude Sonnet 5 --- app/checkout/components/CheckoutContent.tsx | 39 +++++++++++++-------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/app/checkout/components/CheckoutContent.tsx b/app/checkout/components/CheckoutContent.tsx index 8b94900..112b008 100644 --- a/app/checkout/components/CheckoutContent.tsx +++ b/app/checkout/components/CheckoutContent.tsx @@ -142,7 +142,12 @@ export function CheckoutContent({ // fallback for fields somehow never blurred, e.g. autofill). const [fieldErrors, setFieldErrors] = useState>({}); - function setFieldError(name: string, message: string) { + // `refocusEl`, when given, gets focus put right back on it whenever + // `message` is non-empty — a field that just failed its own blur + // validation keeps the cursor instead of letting focus move on to + // wherever the customer tabbed/clicked next, so the correction happens + // immediately instead of being left for a submit attempt to catch later. + function setFieldError(name: string, message: string, refocusEl?: HTMLInputElement) { setFieldErrors((prev) => { if (!message) { if (!(name in prev)) return prev; @@ -153,6 +158,9 @@ export function CheckoutContent({ if (prev[name] === message) return prev; return { ...prev, [name]: message }; }); + if (message && refocusEl) { + refocusEl.focus(); + } } // Address-card fields — controlled (unlike before) so they can be @@ -357,7 +365,7 @@ export function CheckoutContent({ const input = e.target; const value = input.value; const formatError = validateVatIdFormat(value); - setFieldError("vatId", formatError); + setFieldError("vatId", formatError, input); if (!value.trim() || formatError) { setVatIdViesStatus("idle"); return; @@ -431,7 +439,7 @@ export function CheckoutContent({ // in case", only once actually relevant. async function handleEmailBlur(e: React.FocusEvent) { const email = e.target.value.trim(); - setFieldError("email", validateEmailFormat(email)); + setFieldError("email", validateEmailFormat(email), e.target); if (!email || customerEmail || showLogin) return; try { const res = await fetch("/api/account/check-email", { @@ -674,7 +682,7 @@ export function CheckoutContent({ type="text" value={firstName} onChange={(e) => setFirstName(e.target.value)} - onBlur={(e) => setFieldError("firstName", validateRequired("Vorname", e.target.value))} + onBlur={(e) => setFieldError("firstName", validateRequired("Vorname", e.target.value), e.target)} error={fieldErrors.firstName} placeholder="Max" autoComplete="given-name" @@ -686,7 +694,7 @@ export function CheckoutContent({ type="text" value={lastName} onChange={(e) => setLastName(e.target.value)} - onBlur={(e) => setFieldError("lastName", validateRequired("Nachname", e.target.value))} + onBlur={(e) => setFieldError("lastName", validateRequired("Nachname", e.target.value), e.target)} error={fieldErrors.lastName} placeholder="Mustermann" autoComplete="family-name" @@ -784,6 +792,7 @@ export function CheckoutContent({ : e.target.value.length < 8 ? "Passwort muss mindestens 8 Zeichen lang sein." : "", + e.target, ) } error={fieldErrors.password} @@ -805,7 +814,7 @@ export function CheckoutContent({ type="text" value={street} onChange={(e) => setStreet(e.target.value)} - onBlur={(e) => setFieldError("street", validateRequired("Straße und Hausnummer", e.target.value))} + onBlur={(e) => setFieldError("street", validateRequired("Straße und Hausnummer", e.target.value), e.target)} error={fieldErrors.street} placeholder="Musterstraße 1" autoComplete="street-address" @@ -819,7 +828,7 @@ export function CheckoutContent({ type="text" value={zip} onChange={(e) => setZip(e.target.value)} - onBlur={(e) => setFieldError("zip", validateZip(e.target.value, country))} + onBlur={(e) => setFieldError("zip", validateZip(e.target.value, country), e.target)} error={fieldErrors.zip} placeholder="10115" autoComplete="postal-code" @@ -835,7 +844,7 @@ export function CheckoutContent({ type="text" value={city} onChange={(e) => setCity(e.target.value)} - onBlur={(e) => setFieldError("city", validateRequired("Ort", e.target.value))} + onBlur={(e) => setFieldError("city", validateRequired("Ort", e.target.value), e.target)} error={fieldErrors.city} placeholder="Berlin" autoComplete="address-level2" @@ -878,7 +887,7 @@ export function CheckoutContent({ type="text" value={shippingFirstName} onChange={(e) => setShippingFirstName(e.target.value)} - onBlur={(e) => setFieldError("shippingFirstName", validateRequired("Vorname", e.target.value))} + onBlur={(e) => setFieldError("shippingFirstName", validateRequired("Vorname", e.target.value), e.target)} error={fieldErrors.shippingFirstName} placeholder="Max" autoComplete="off" @@ -889,7 +898,7 @@ export function CheckoutContent({ type="text" value={shippingLastName} onChange={(e) => setShippingLastName(e.target.value)} - onBlur={(e) => setFieldError("shippingLastName", validateRequired("Nachname", e.target.value))} + onBlur={(e) => setFieldError("shippingLastName", validateRequired("Nachname", e.target.value), e.target)} error={fieldErrors.shippingLastName} placeholder="Mustermann" autoComplete="off" @@ -931,7 +940,7 @@ export function CheckoutContent({ type="text" value={shippingStreet} onChange={(e) => setShippingStreet(e.target.value)} - onBlur={(e) => setFieldError("shippingStreet", validateRequired("Straße und Hausnummer", e.target.value))} + onBlur={(e) => setFieldError("shippingStreet", validateRequired("Straße und Hausnummer", e.target.value), e.target)} error={fieldErrors.shippingStreet} placeholder="Musterstraße 1" autoComplete="off" @@ -945,7 +954,7 @@ export function CheckoutContent({ type="text" value={shippingPackstationNumber} onChange={(e) => setShippingPackstationNumber(e.target.value)} - onBlur={(e) => setFieldError("shippingPackstationNumber", validatePackstationNumber(e.target.value))} + onBlur={(e) => setFieldError("shippingPackstationNumber", validatePackstationNumber(e.target.value), e.target)} error={fieldErrors.shippingPackstationNumber} inputMode="numeric" placeholder="123" @@ -960,7 +969,7 @@ export function CheckoutContent({ type="text" value={shippingPostNumber} onChange={(e) => setShippingPostNumber(e.target.value)} - onBlur={(e) => setFieldError("shippingPostNumber", validatePostNumber(e.target.value))} + onBlur={(e) => setFieldError("shippingPostNumber", validatePostNumber(e.target.value), e.target)} error={fieldErrors.shippingPostNumber} inputMode="numeric" placeholder="1234567890" @@ -978,7 +987,7 @@ export function CheckoutContent({ type="text" value={shippingZip} onChange={(e) => setShippingZip(e.target.value)} - onBlur={(e) => setFieldError("shippingZip", validateZip(e.target.value, shippingCountry))} + onBlur={(e) => setFieldError("shippingZip", validateZip(e.target.value, shippingCountry), e.target)} error={fieldErrors.shippingZip} placeholder="10115" autoComplete="off" @@ -993,7 +1002,7 @@ export function CheckoutContent({ type="text" value={shippingCity} onChange={(e) => setShippingCity(e.target.value)} - onBlur={(e) => setFieldError("shippingCity", validateRequired("Ort", e.target.value))} + onBlur={(e) => setFieldError("shippingCity", validateRequired("Ort", e.target.value), e.target)} error={fieldErrors.shippingCity} placeholder="Berlin" autoComplete="off"