View Issue Details

IDProjectCategoryView StatusLast Update
0012058Talerotherpublic2026-11-10 23:43
Reporterbot-cg [agent] Assigned ToResolution deadline
ImplementerReviewer 
Release signed byDeployed by 
VerifierCommunicatorCM_7  
Data protection authority notification requiredNo  
CSIRT notification requiredNo  
Estimated effort (hours)Actual effort (hours)0 
Fast-trackNo
Work packagenone
PrioritynormalSeverityminorReproducibilityhave not tried
Status newResolutionopen 
Summary0012058: web-util: Terms of service form: 'Read terms' is not focused, and the terms reader moves focus back to its × button whenever the page re-renders
DescriptionWhat happens:
(1) AcceptTosForm handled `focus` with `autofocus={props.focus}` on the 'Read terms' button. Preact only sets the attribute, which browsers ignore once anything has had focus, so a KYC accept-ToS step mounted with `<FormUI focus>` left focus on the trigger (measured: BUTTON#mount). (2) TermsReaderDialog's open effect depended on `[onClose]`, but AcceptTosForm passes a new `closeReader` function on every render. Each parent re-render (loading finished, document stored, 'downloaded' recorded in the form model, any re-render of the KYC page) ran the cleanup, which focused 'Read terms' behind the overlay, then ran the effect again, which focused the × close button. Measured: open the reader, move focus to 'Done' while the terms load, then the terms arrive -> activeElement is the × 'Close terms of service' button again.

Expected:
'Read terms' gets focus when the form is flagged `focus`. Inside the reader, focus moves to × once on open and then stays wherever the user put it; on close it goes back to 'Read terms'.

Where:
  - packages/web-util/src/forms/AcceptTosForm.tsx:121
  - packages/web-util/src/forms/AcceptTosForm.tsx:179
  - packages/web-util/src/forms/AcceptTosForm.tsx:367

Measured in headless Chromium (document.activeElement):
  before: initial (FormUI focus): BUTTON#mount ('Read terms' not focused); reader after terms load: BUTTON[aria-label='Close terms of service'] although the user had moved to 'Done'
  after: initial: BUTTON 'Read terms'; reader after terms load: BUTTON 'Done' (kept); Escape -> BUTTON 'Read terms again'

Proposed fix:
The read button uses useAutoFocus(props.focus, ...) instead of the inert `autofocus` attribute. TermsReaderDialog reads onClose through a ref and runs its open/close effect once (deps []), so parent re-renders no longer re-focus × or toggle body scroll locking. Needs the useAutoFocus helper from web-util--form-focus.diff.

Patch against taler-typescript-core master attached; the same change is committed on branch dev/bots/autofocus (durian). Screenshots attached (focused element outlined in red): 'before' is master, 'after' is with the patch.

Verified: tsc ok (no new errors); eslint clean; AcceptTosForm.test (3 tests) pass; Chromium: tos and tos-reader scenarios, see also shots/web-util--tos--before.png and --after.png for initial focus
TagsNo tags attached.
Attached Files
web-util--accept-tos.diff (2,684 bytes)   
diff --git a/packages/web-util/src/forms/AcceptTosForm.tsx b/packages/web-util/src/forms/AcceptTosForm.tsx
index b2ebdcaa9..d9e74e7a4 100644
--- a/packages/web-util/src/forms/AcceptTosForm.tsx
+++ b/packages/web-util/src/forms/AcceptTosForm.tsx
@@ -1,6 +1,7 @@
 import { TalerFormAttributes } from "@gnu-taler/taler-util";
 import { Fragment, type ComponentChildren, h, type VNode } from "preact";
 import { useEffect, useRef, useState } from "preact/hooks";
+import { useAutoFocus } from "../components/utils.js";
 import { useTranslationContext } from "../context/translation.js";
 import type { FormModel } from "../hooks/useForm.js";
 import {
@@ -114,6 +115,11 @@ function TermsReaderDialog(props: {
   onClose: () => void;
 }): VNode {
   const { onClose } = props;
+  // The parent passes a new onClose on every render; read it through a ref
+  // so that a re-render (e.g. when the terms finish loading) does not run the
+  // open/close effect again and pull focus back to the close button.
+  const onCloseRef = useRef(onClose);
+  onCloseRef.current = onClose;
   const { i18n } = useTranslationContext();
   const dialogRef = useRef<HTMLDivElement>(null);
   const closeButtonRef = useRef<HTMLButtonElement>(null);
@@ -143,7 +149,7 @@ function TermsReaderDialog(props: {
     const onKeyDown = (event: KeyboardEvent) => {
       if (event.key === "Escape") {
         event.preventDefault();
-        onClose();
+        onCloseRef.current();
         return;
       }
       const dialog = dialogRef.current;
@@ -170,7 +176,7 @@ function TermsReaderDialog(props: {
       document.body.style.overflow = previousOverflow;
       previousFocus.current?.focus();
     };
-  }, [onClose]);
+  }, []);
 
   async function downloadPdf(): Promise<void> {
     setDownloadState("loading");
@@ -358,6 +364,7 @@ export function AcceptTosForm(props: {
   const [error, setError] = useState<ReaderError>();
   const request = useRef<AbortController>();
   const readButton = useRef<HTMLButtonElement>(null);
+  useAutoFocus(props.focus, () => readButton.current);
 
   useEffect(
     () => () => {
@@ -460,7 +467,6 @@ export function AcceptTosForm(props: {
             disabled={props.disabled}
             class="mt-5 inline-flex min-h-11 items-center justify-center rounded-md bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 disabled:cursor-default disabled:opacity-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
             onClick={openReader}
-            autofocus={props.focus}
           >
             {hasRead ? readAgainLabel : readLabel}
           </button>
web-util--accept-tos.diff (2,684 bytes)   
web-util--tos-reader--after.png (31,995 bytes)   
web-util--tos-reader--after.png (31,995 bytes)   

Required notifications

Address or linkRemarkAdded byNotifiedActions

Relationships

child of 0012024 new Web UIs (bank, wallet, exchange AML/KYC, challenger, auditor, web-util forms): dialogs and single-purpose forms open without keyboard focus on the obvious field 

Activities

Issue History

Date ModifiedUsernameFieldChange
2026-11-10 23:42bot-cgNew Issue
2026-11-10 23:42bot-cgRelationship addedchild of 0012024
2026-11-10 23:43bot-cgFile Added: web-util--accept-tos.diff
2026-11-10 23:43bot-cgFile Added: web-util--tos-reader--before.png
2026-11-10 23:43bot-cgFile Added: web-util--tos-reader--after.png