View Issue Details

IDProjectCategoryView StatusLast Update
0012005Talermerchant portal webuipublic2026-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 
Summary0012005: Merchant: 'Pair a till' dialog does not focus Device Name, and its credential step drops focus (Escape stops working)
DescriptionWhat happens:
Access for machines -> 'Pair a till' opens a hand-rolled role=dialog (not Modal) with no focus handling: focus stays on the 'Pair a till' button behind the overlay (measured BUTTON 'Pair a till'). After submitting, the form (holding focus) is replaced by the QR/credential step, so focus falls to BODY; the dialog's Escape handler sits on the dialog element, so Escape no longer does anything (measured: Escape did not show the 'Close without pairing?' warning).

Expected:
Opening focuses #pair_dev_name; the credential step puts focus inside the dialog (its heading) so Escape and Tab work, without preselecting an action on a credential shown only once.

Where:
  - packages/taler-merchant-webui/src/screens/AccessScreen.tsx:137
  - packages/taler-merchant-webui/src/screens/AccessScreen.tsx:499
  - packages/taler-merchant-webui/src/screens/AccessScreen.tsx:622

Measured in headless Chromium (document.activeElement):
  before: form step: BUTTON 'Pair a till' (behind overlay); QR step: BODY, Escape inert
  after: form step: INPUT#pair_dev_name; QR step: H3 'Scan this with the till app', Escape shows the close warning

Proposed fix:
useEffect on [showPairModal, pairStep] focuses a ref on #pair_dev_name for the form step and the step heading (tabIndex=-1, focus:outline-none, same pattern as the subscription wizard's step heading) for the QR step. Runs only when the dialog opens or the step changes.

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; eslint clean; screenshots access-pair-form and access-pair-qr before/after; Escape check false->true in the Chromium flow; tutorial preview that mounts this dialog open on the QR step does not receive focus (relies on the LiveComponentPreview inert-on-mount change, see notes)
TagsNo tags attached.
Attached Files
merchant-a--access-pair-till-dialog.diff (2,685 bytes)   
diff --git a/packages/taler-merchant-webui/src/screens/AccessScreen.tsx b/packages/taler-merchant-webui/src/screens/AccessScreen.tsx
index 369270c33..97f8f35ca 100644
--- a/packages/taler-merchant-webui/src/screens/AccessScreen.tsx
+++ b/packages/taler-merchant-webui/src/screens/AccessScreen.tsx
@@ -16,7 +16,7 @@
 
 import { FormActionFooter, useFormFeedback } from "../ui/FormActionFooter.js";
 import type { VNode } from "preact";
-import { useEffect, useState } from "preact/hooks";
+import { useEffect, useRef, useState } from "preact/hooks";
 import { useLocation } from "wouter-preact";
 import { Header } from "../ui/Header.js";
 import { TalerQrCode } from "../ui/TalerQrCode.js";
@@ -134,6 +134,18 @@ export function AccessScreen({
     setPairingError(undefined);
   }, [issuedPairingCredential, setPairingError]);
 
+  // This dialog is drawn here rather than by Modal, so it places focus itself:
+  // in the device name when it opens, and on the heading of the credential
+  // step, whose arrival removes the focused form and would otherwise drop
+  // focus to the page, out of reach of the dialog's Escape handler.
+  const pairNameInput = useRef<HTMLInputElement>(null);
+  const pairQrHeading = useRef<HTMLHeadingElement>(null);
+  useEffect(() => {
+    if (!showPairModal) return;
+    if (pairStep === "form") pairNameInput.current?.focus();
+    else pairQrHeading.current?.focus();
+  }, [showPairModal, pairStep]);
+
   const displayTokens = tokensResource?.data ?? propTokens ?? [];
   const isInitiallyLoading =
     tokensResource?.isLoading === true && tokensResource.data === undefined;
@@ -484,6 +496,7 @@ export function AccessScreen({
                     {t`Device Name`} <span class="text-red-500">*</span>
                   </label>
                   <input
+                    ref={pairNameInput}
                     id="pair_dev_name"
                     type="text"
                     required
@@ -606,7 +619,11 @@ export function AccessScreen({
             ) : (
               <div class="space-y-4 text-center">
                 <div>
-                  <h3 class="text-lg font-bold text-gray-900">{t`Scan this with the till app`}</h3>
+                  <h3
+                    ref={pairQrHeading}
+                    tabIndex={-1}
+                    class="text-lg font-bold text-gray-900 focus:outline-none"
+                  >{t`Scan this with the till app`}</h3>
                   <p class="text-xs text-gray-600 mt-1 leading-relaxed bg-amber-50 p-2 rounded border border-amber-200">
                     {t`ℹ️ This credential is shown once. Anyone who has it can use the granted till access.`}
                   </p>

Required notifications

Address or linkRemarkAdded byNotifiedActions

Relationships

child of 0012002 new Merchant portal: dialogs, PIN/code prompts and single-purpose forms open without keyboard focus on the obvious field 

Activities

Issue History

Date ModifiedUsernameFieldChange
2026-11-10 23:41bot-cgNew Issue
2026-11-10 23:41bot-cgRelationship addedchild of 0012002
2026-11-10 23:43bot-cgFile Added: merchant-a--access-pair-till-dialog.diff
2026-11-10 23:43bot-cgFile Added: merchant-a--access-pair-form--before.png
2026-11-10 23:43bot-cgFile Added: merchant-a--access-pair-form--after.png