commit 40d4492e1b52bc77b1d06e27cfe2083d03e4f743
parent f86b75617b8e41cd45d4a20976e19bceda09a3ed
Author: Florian Dold <dold@taler.net>
Date: Sun, 9 Aug 2026 14:15:52 +0200
Fix button error notification dismissal
Diffstat:
2 files changed, 49 insertions(+), 2 deletions(-)
diff --git a/packages/web-util/src/components/Button.tsx b/packages/web-util/src/components/Button.tsx
@@ -62,14 +62,12 @@ export function Button({
const [running, setRunning] = useState(false);
const [failed, setFailed] = useState(false);
const { notification: ns } = useNotificationContext();
-
const notification = ns.length > 0 ? ns[0] : undefined;
// if the button is in failed state and the user
// change the state of the form that affect this action handler
// the remove the failed state for faster submit
useEffect(() => {
if (failed) {
- notification?.acknowledge();
setFailed(false);
}
}, onClick?.args ?? []);
diff --git a/packages/web-util/src/hooks/useNotifications.stories.tsx b/packages/web-util/src/hooks/useNotifications.stories.tsx
@@ -30,6 +30,10 @@ import { Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { Attention } from "../components/Attention.js";
import { Button } from "../components/Button.js";
+import {
+ NotificationProvider,
+ useNotificationContext,
+} from "../context/notification.js";
import { delayMs } from "./useAsync.js";
import * as tests from "../tests/hook.js";
import {
@@ -279,6 +283,51 @@ export const messages = tests.createExample(() => {
);
}, {});
+export const recreatedArgumentsKeepErrorVisible = tests.createExample(() => {
+ return (
+ <NotificationProvider>
+ <RecreatedArgumentsKeepErrorVisible />
+ </NotificationProvider>
+ );
+}, {});
+
+function RecreatedArgumentsKeepErrorVisible(): VNode {
+ const { actionHandler, notification, showError } = useNotificationContext();
+ const action = actionHandler(
+ async function (_ct, _input: { value: string }) {
+ return opKnownFailure({} as any, "failure");
+ },
+ // Deliberately recreate the argument object on every render. This mirrors
+ // the password wrapper used by the AML session unlock form.
+ [{ value: "same value" }],
+ );
+ action.onFail = showError(
+ "the operation failed" as TranslatedString,
+ () => "the error should remain visible" as TranslatedString,
+ );
+
+ return (
+ <div>
+ <p>
+ Click the button. The error must remain visible after the failed action
+ rerenders the component.
+ </p>
+ <Button onClick={action}>fail</Button>
+ {notification.length ? (
+ <Attention
+ type="danger"
+ title={notification[0].message.title}
+ onClose={notification[0].acknowledge}
+ >
+ {notification[0].message.type === "error"
+ ? notification[0].message.description
+ : undefined}
+ </Attention>
+ ) : undefined}
+ </div>
+ );
+}
+
export const confirm = tests.createExample(() => {
const safe = newSafeHandlerBuilder({});
const [name, setName] = useState("");