View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0012054 | Taler | wallet (WebExtension) | public | 2026-11-10 23:42 | 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 | tweak | Reproducibility | have not tried |
| Status | new | Resolution | open | ||
| Summary | 0012054: Wallet: shared ConfirmationDialog focuses an extra body action instead of Cancel (durability prompt) | ||||
| Description | What happens: ConfirmationDialog focused the first focusable element in the dialog. For the PWA durability prompt ('Protect wallet data before continuing', src/platform/durability.tsx, shown before money actions when storage is not persistent) the children contain 'Install PWA' and 'Database export and import' buttons before the actions, so focus landed on 'Install PWA' (or on 'Database export and import', which cancels the pending action and navigates away, when install is unavailable). Rendered with the same children: activeElement BUTTON 'Install PWA'. Expected: A field the dialog asks for if any (e.g. the 'I understand' acknowledgement checkbox), otherwise the safe Cancel button; never the confirming action or an extra action. Where: - packages/wallet-webui/src/ui/ConfirmationDialog.tsx:41 Measured in headless Chromium (document.activeElement): before: BUTTON 'Install PWA' after: BUTTON 'Cancel' Proposed fix: On open, focus the first enabled input/select/textarea in the dialog, else the cancel button (first button of the actions row), else the dialog container. Tab trapping is unchanged. 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 (0 errors in wallet-webui with workspace sources; plain `tsc --noEmit -p .` output identical to baseline, whose errors come from stale taler-util/wallet-core lib/); eslint + prettier clean on changed files; package tests screens/exchange-removal/payment-entry/routing bundled from src: 89/95 pass, the same 6 App-level demo tests fail before and after; scratch test: plain -> Cancel, extra button -> Cancel, checkbox -> checkbox; exchange-delete story still focuses its acknowledgement checkbox (wallet--exchange-delete-dialog--*.png); demo reset dialog still Cancel. Call sites: durability prompt (changed), TransactionDetailScreen x2, SettingsScreen x3, ExchangeDetailScreen x2, BankAccountsScreen, PaymentScreen, DemoBanner (unchanged result). | ||||
| Tags | No tags attached. | ||||
| Attached Files | wallet--confirmation-dialog.diff (1,883 bytes)
diff --git a/packages/wallet-webui/src/ui/ConfirmationDialog.tsx b/packages/wallet-webui/src/ui/ConfirmationDialog.tsx
index 32844defa..72ce55961 100644
--- a/packages/wallet-webui/src/ui/ConfirmationDialog.tsx
+++ b/packages/wallet-webui/src/ui/ConfirmationDialog.tsx
@@ -25,6 +25,7 @@ export function ConfirmationDialog(props: {
onConfirm: () => void;
}) {
const dialogRef = useRef<HTMLDivElement>(null);
+ const actionsRef = useRef<HTMLDivElement>(null);
const previousFocus = useRef<HTMLElement | null>(null);
const onCancel = useRef(props.onCancel);
const working = useRef(props.working);
@@ -38,8 +39,15 @@ export function ConfirmationDialog(props: {
? document.activeElement
: null;
const dialog = dialogRef.current;
- const first = dialog ? focusableElements(dialog)[0] : undefined;
- first?.focus();
+ // Start on the first field the dialog asks for, otherwise on the cancel
+ // button: neither the confirming action nor an extra action placed in
+ // the body should be what Enter triggers by default.
+ const cancel = actionsRef.current?.querySelector("button");
+ const initial =
+ dialog?.querySelector<HTMLElement>(
+ "input:not([disabled]), select:not([disabled]), textarea:not([disabled])",
+ ) ?? (cancel && !cancel.disabled ? cancel : dialog);
+ initial?.focus();
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape" && !working.current) {
event.preventDefault();
@@ -98,7 +106,7 @@ export function ConfirmationDialog(props: {
{props.description}
</div>
{props.children}
- <div class="mt-6 flex flex-wrap justify-end gap-3">
+ <div ref={actionsRef} class="mt-6 flex flex-wrap justify-end gap-3">
<Button
tone="secondary"
onClick={props.onCancel}
| ||||
| 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 |
| Date Modified | Username | Field | Change |
|---|---|---|---|
| 2026-11-10 23:42 | bot-cg | New Issue | |
| 2026-11-10 23:42 | bot-cg | Relationship added | child of 0012024 |
| 2026-11-10 23:43 | bot-cg | File Added: wallet--confirmation-dialog.diff | |
| 2026-11-10 23:43 | bot-cg | File Added: wallet--confirmation-dialog--before.png | |
| 2026-11-10 23:43 | bot-cg | File Added: wallet--confirmation-dialog--after.png |