taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 6e73359692af26551ef2974058e93acd9fca7828
parent a651ff0f5982d7b08e904f850e0fb6966484c0ab
Author: Florian Dold <dold@taler.net>
Date:   Sun, 23 Aug 2026 17:42:38 +0200

web-util: avoid duplicate list add action

Diffstat:
Mpackages/web-util/src/forms/fields/InputArray.test.tsx | 27+++++++++++++--------------
Mpackages/web-util/src/forms/fields/InputArray.tsx | 55+++++++++++++++++++++++++++++++++++++------------------
2 files changed, 50 insertions(+), 32 deletions(-)

diff --git a/packages/web-util/src/forms/fields/InputArray.test.tsx b/packages/web-util/src/forms/fields/InputArray.test.tsx @@ -95,6 +95,7 @@ test("array items can be added, edited, cancelled, and deleted", async () => { const originalAddButton = view.getByRole("button", { name: "Add item" }); originalAddButton.click(); await view.findByRole("button", { name: "Cancel" }); + assert.equal(view.getAllByRole("button", { name: "Add item" }).length, 1); const nameInput = view.container.querySelector<HTMLInputElement>('input[name="name"]'); @@ -115,15 +116,10 @@ test("array items can be added, edited, cancelled, and deleted", async () => { addressInput.value = "Main Street 12, 8000 Zürich"; addressInput.dispatchEvent(new window.Event("change", { bubbles: true })); await new Promise((resolve) => setTimeout(resolve, 0)); - let confirmAddButton: HTMLElement | undefined; + const confirmAddButton = view.getByRole("button", { name: "Add item" }); await eventually(() => { - const addButtons = view.getAllByRole("button", { name: "Add item" }); - confirmAddButton = addButtons.find( - (button: HTMLElement) => !(button as HTMLButtonElement).disabled, - ); - assert.ok(confirmAddButton); + assert.equal((confirmAddButton as HTMLButtonElement).disabled, false); }); - assert.ok(confirmAddButton); confirmAddButton.click(); await eventually(() => @@ -135,15 +131,14 @@ test("array items can be added, edited, cancelled, and deleted", async () => { ]), ); - await eventually(() => - assert.equal( - (view.getByRole("button", { name: "Add item" }) as HTMLButtonElement) - .disabled, - false, - ), - ); + await eventually(() => { + const addButton = view.getByRole("button", { name: "Add item" }); + assert.equal((addButton as HTMLButtonElement).disabled, false); + assert.equal(window.document.activeElement === addButton, true); + }); view.getByRole("button", { name: "Add item" }).click(); await view.findByRole("button", { name: "Cancel" }); + assert.equal(view.getAllByRole("button", { name: "Add item" }).length, 1); const secondNameInput = view.container.querySelector<HTMLInputElement>('input[name="name"]'); assert.ok(secondNameInput); @@ -151,6 +146,10 @@ test("array items can be added, edited, cancelled, and deleted", async () => { secondNameInput.dispatchEvent(new window.Event("change", { bubbles: true })); view.getByRole("button", { name: "Cancel" }).click(); await eventually(() => assert.equal(result.people.length, 1)); + await eventually(() => { + const addButton = view.getByRole("button", { name: "Add item" }); + assert.equal(window.document.activeElement === addButton, true); + }); await eventually(() => assert.equal( ( diff --git a/packages/web-util/src/forms/fields/InputArray.tsx b/packages/web-util/src/forms/fields/InputArray.tsx @@ -32,6 +32,7 @@ function ArrayForm({ onRemove, onConfirm, isNew, + showTopBorder, }: { fields: UIFormElementConfig[]; selected: Record<string, unknown> | undefined; @@ -39,6 +40,7 @@ function ArrayForm({ onRemove: () => void; onConfirm: (r: RecursivePartial<FormType>) => void; isNew: boolean; + showTopBorder: boolean; }): VNode { const { i18n } = useTranslationContext(); const design = useMemo( @@ -58,7 +60,14 @@ function ArrayForm({ }, []); return ( - <div ref={editorRef} class="border-t border-gray-200 px-3 py-4 sm:px-4"> + <div + ref={editorRef} + class={ + showTopBorder + ? "border-t border-gray-200 px-3 py-4 sm:px-4" + : "px-3 py-4 sm:px-4" + } + > <div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6"> <RenderAllFieldsByUiConfig fields={convertFormConfigToUiField(i18n, "root", fields, form.model)} @@ -142,6 +151,7 @@ export function InputArray( const [dirty, setDirty] = useState(false); const [editor, setEditor] = useState<Editor | undefined>(); const returnFocusRef = useRef<HTMLButtonElement | null>(null); + const addButtonRef = useRef<HTMLButtonElement | null>(null); const generatedId = useId(); const fieldId = `array-${generatedId}`; const labelId = `${fieldId}-label`; @@ -168,9 +178,15 @@ export function InputArray( .join(" "); function closeEditor(): void { + const restoreAddFocus = editor?.type === "add"; setDirty(true); setEditor(undefined); - setTimeout(() => returnFocusRef.current?.focus(), 0); + setTimeout(() => { + (restoreAddFocus + ? addButtonRef.current + : returnFocusRef.current + )?.focus(); + }, 0); } function labelForItem(item: Record<string, unknown>, index: number) { @@ -194,6 +210,7 @@ export function InputArray( key={isNew ? "add" : `edit-${editor.index}`} fields={fields} isNew={isNew} + showTopBorder={isNew ? list.length > 0 : true} selected={selected} onRemove={() => { if (editor.type !== "edit") return; @@ -280,23 +297,25 @@ export function InputArray( </ul> )} - {!props.disabled && ( - <div class={list.length > 0 ? "border-t border-gray-200 p-3" : "p-3"}> - <button - id={`${fieldId}-add`} - type="button" - disabled={editor !== undefined} - onClick={(event) => { - returnFocusRef.current = event.currentTarget; - setEditor({ type: "add" }); - }} - class="w-full rounded-md border border-dashed border-gray-300 px-3 py-2.5 text-sm font-semibold text-indigo-700 hover:border-indigo-300 hover:bg-indigo-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:cursor-not-allowed disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-400" + {!props.disabled && + (editor?.type === "add" ? ( + renderEditor(undefined) + ) : ( + <div + class={list.length > 0 ? "border-t border-gray-200 p-3" : "p-3"} > - <i18n.Translate>Add item</i18n.Translate> - </button> - {editor?.type === "add" ? renderEditor(undefined) : undefined} - </div> - )} + <button + ref={addButtonRef} + id={`${fieldId}-add`} + type="button" + disabled={editor !== undefined} + onClick={() => setEditor({ type: "add" })} + class="w-full rounded-md border border-dashed border-gray-300 px-3 py-2.5 text-sm font-semibold text-indigo-700 hover:border-indigo-300 hover:bg-indigo-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:cursor-not-allowed disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-400" + > + <i18n.Translate>Add item</i18n.Translate> + </button> + </div> + ))} </div> </div> );