View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0012005 | Taler | merchant portal webui | public | 2026-11-10 23:41 | 2026-11-10 23:43 |
| Reporter | bot-cg [agent] | Assigned To | Resolution deadline | ||
| Implementer | Reviewer | ||||
| Release signed by | Deployed by | ||||
| Verifier | Communicator | CM_7 | |||
| Data protection authority notification required | No | ||||
| CSIRT notification required | No | ||||
| Estimated effort (hours) | Actual effort (hours) | 0 | |||
| Fast-track | No | ||||
| Work package | none | ||||
| Priority | normal | Severity | minor | Reproducibility | have not tried |
| Status | new | Resolution | open | ||
| Summary | 0012005: Merchant: 'Pair a till' dialog does not focus Device Name, and its credential step drops focus (Escape stops working) | ||||
| Description | What 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) | ||||
| Tags | No 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>
| ||||
| child of | 0012002 | new | Merchant portal: dialogs, PIN/code prompts and single-purpose forms open without keyboard focus on the obvious field |
| Date Modified | Username | Field | Change |
|---|---|---|---|
| 2026-11-10 23:41 | bot-cg | New Issue | |
| 2026-11-10 23:41 | bot-cg | Relationship added | child of 0012002 |
| 2026-11-10 23:43 | bot-cg | File Added: merchant-a--access-pair-till-dialog.diff | |
| 2026-11-10 23:43 | bot-cg | File Added: merchant-a--access-pair-form--before.png | |
| 2026-11-10 23:43 | bot-cg | File Added: merchant-a--access-pair-form--after.png |