diff --git a/packages/taler-merchant-webui/src/screens/ForgotPasswordScreen.tsx b/packages/taler-merchant-webui/src/screens/ForgotPasswordScreen.tsx index ad86e8b58..541b87a2f 100644 --- a/packages/taler-merchant-webui/src/screens/ForgotPasswordScreen.tsx +++ b/packages/taler-merchant-webui/src/screens/ForgotPasswordScreen.tsx @@ -16,7 +16,7 @@ import { ActionFooter } from "../ui/FormActionFooter.js"; import type { VNode } from "preact"; -import { useState } from "preact/hooks"; +import { useEffect, useRef, useState } from "preact/hooks"; import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { TalerLogo } from "../ui/TalerLogo.js"; import { CopyErrorButton } from "../ui/CopyErrorButton.js"; @@ -26,6 +26,7 @@ import { } from "../stores/session.js"; import { useTranslation } from "../context/translation.js"; import { formatErrorMessage } from "../utils/errors.js"; +import { focusFirstEmptyField } from "../utils/focus.js"; import { RawLayout } from "../ui/RawLayout.js"; import { PasswordInput } from "../ui/PasswordInput.js"; @@ -64,6 +65,9 @@ export function ForgotPasswordScreen({ const [errorMsg, setErrorMsg] = useState(""); const [rawError, setRawError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); + // Start on the account, or on the new password when the account is known. + const form = useRef(null); + useEffect(() => focusFirstEmptyField(form.current), []); const handleSubmit = async (e: Event) => { e.preventDefault(); @@ -135,7 +139,7 @@ export function ForgotPasswordScreen({ } >
-
+
)} - + {step === "form" && ( <>
@@ -952,12 +972,14 @@ export function SelfProvisionScreen({
{ + returnFocusId.current = "signup-email"; setStep("form"); setErrorMsg(""); }} @@ -992,12 +1014,14 @@ export function SelfProvisionScreen({
{ + returnFocusId.current = "signup-phone"; setStep("form"); setErrorMsg(""); }} diff --git a/packages/taler-merchant-webui/src/screens/SignInScreen.tsx b/packages/taler-merchant-webui/src/screens/SignInScreen.tsx index 4df80b062..99446f273 100644 --- a/packages/taler-merchant-webui/src/screens/SignInScreen.tsx +++ b/packages/taler-merchant-webui/src/screens/SignInScreen.tsx @@ -28,6 +28,7 @@ import { CopyErrorButton } from "../ui/CopyErrorButton.js"; import { FooterControls } from "../ui/FooterControls.js"; import { useTranslation } from "../context/translation.js"; import { formatErrorMessage } from "../utils/errors.js"; +import { focusCheckedRadio, focusFirstEmptyField } from "../utils/focus.js"; import { currentAccount, lastMerchantAccountStore, @@ -224,6 +225,15 @@ export function SignInScreen({ const maxAttemptsReached = attemptsCount >= 5; + // Each step swaps the form's fields, so put the cursor on the one it asks + // for: the account (or, when it is remembered, the password), or the chosen + // verification method. The code step's input focuses itself. + const form = useRef(null); + useEffect(() => { + if (step === "credentials") focusFirstEmptyField(form.current); + else if (step === "channel_select") focusCheckedRadio(form.current); + }, [step]); + /** * Send the code when the screen opens already asking for one. * @@ -547,7 +557,7 @@ export function SignInScreen({ )} - + {step === "credentials" && ( <>
@@ -671,6 +681,7 @@ export function SignInScreen({
(null); + // The code is the only thing this step asks for, so typing should land in it + // straight away. Preact does not act on `autoFocus` for elements rendered + // after page load, hence the explicit call; it runs once per mount, so a + // parent that moves to the next challenge remounts this with a new `key`. + useEffect(() => { + input.current?.focus(); + }, []); return (
@@ -52,6 +61,7 @@ export function TanCodeInputGroup({ {label || t`Verification code`} + */ + +/** + * Focus the field a person has to fill next in `root`: the first enabled text + * entry that is still empty, else the first one. Prefilled values (a + * remembered account name, a restored draft) are skipped so the cursor lands + * on what is actually missing. + * + * Preact does not act on `autoFocus` for elements rendered after the initial + * page load, so screens call this when a step appears. + */ +export function focusFirstEmptyField( + root: ParentNode | null | undefined, +): void { + if (!root) return; + const fields = Array.from( + root.querySelectorAll( + 'input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]):not([type="button"]):not([type="submit"]), textarea', + ), + ).filter((field) => !field.disabled && !field.readOnly); + (fields.find((field) => field.value === "") ?? fields[0])?.focus(); +} + +/** Focus the chosen option of a radio group in `root`, else the first one. */ +export function focusCheckedRadio(root: ParentNode | null | undefined): void { + if (!root) return; + ( + root.querySelector('input[type="radio"]:checked') ?? + root.querySelector('input[type="radio"]') + )?.focus(); +}