Refocus a checkout field automatically when its blur validation fails

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 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-23 19:54:14 +00:00
parent 0c9050cc8a
commit 50db2fcb4b
+24 -15
View File
@@ -142,7 +142,12 @@ export function CheckoutContent({
// fallback for fields somehow never blurred, e.g. autofill).
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
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<HTMLInputElement>) {
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"