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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user