diff --git a/frontend/src/widgets/ArtifactGateWidget.tsx b/frontend/src/widgets/ArtifactGateWidget.tsx index 7770fd84..641ac6b0 100644 --- a/frontend/src/widgets/ArtifactGateWidget.tsx +++ b/frontend/src/widgets/ArtifactGateWidget.tsx @@ -53,7 +53,7 @@ export function ArtifactGateWidget({ descriptor, onRespond }: WidgetProps) { onRespond({ id: descriptor.id, control: c })} + onControl={(c, t) => onRespond({ id: descriptor.id, control: c, ...(t !== undefined ? { text: t } : {}) })} /> ); diff --git a/frontend/src/widgets/MultiselectWidget.tsx b/frontend/src/widgets/MultiselectWidget.tsx index 66b608c1..afecd30f 100644 --- a/frontend/src/widgets/MultiselectWidget.tsx +++ b/frontend/src/widgets/MultiselectWidget.tsx @@ -57,7 +57,7 @@ export function MultiselectWidget({ descriptor, onRespond }: WidgetProps) { onRespond({ id: descriptor.id, control: c })} + onControl={(c, t) => onRespond({ id: descriptor.id, control: c, ...(t !== undefined ? { text: t } : {}) })} /> ); diff --git a/frontend/src/widgets/ReservedControls.test.tsx b/frontend/src/widgets/ReservedControls.test.tsx new file mode 100644 index 00000000..e607e7ec --- /dev/null +++ b/frontend/src/widgets/ReservedControls.test.tsx @@ -0,0 +1,23 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { ReservedControls } from "./ReservedControls"; + +test("back and exit fire immediately with no text", async () => { + const onControl = vi.fn(); + render(); + await userEvent.click(screen.getByRole("button", { name: /go back/i })); + expect(onControl).toHaveBeenCalledWith("back"); + await userEvent.click(screen.getByRole("button", { name: /^exit$/i })); + expect(onControl).toHaveBeenCalledWith("exit"); +}); + +test("other reveals a textarea and emits freetext with the typed text", async () => { + const onControl = vi.fn(); + render(); + await userEvent.click(screen.getByRole("button", { name: /other — specify/i })); + // clicking Other does NOT emit a control yet — it reveals the input + expect(onControl).not.toHaveBeenCalled(); + await userEvent.type(screen.getByRole("textbox"), "usa la tabella X"); + await userEvent.click(screen.getByRole("button", { name: /send/i })); + expect(onControl).toHaveBeenCalledWith("freetext", "usa la tabella X"); +}); diff --git a/frontend/src/widgets/ReservedControls.tsx b/frontend/src/widgets/ReservedControls.tsx index 7311733a..7fd37e7f 100644 --- a/frontend/src/widgets/ReservedControls.tsx +++ b/frontend/src/widgets/ReservedControls.tsx @@ -1,7 +1,42 @@ +import { useState } from "react"; + const LABELS: Record = { back: "Go back", exit: "Exit", other: "Other — specify" }; -export function ReservedControls({ reserved, onControl }: { reserved?: string[]; onControl: (c: string) => void }) { + +export function ReservedControls({ + reserved, + onControl, +}: { + reserved?: string[]; + onControl: (c: string, text?: string) => void; +}) { + const [otherOpen, setOtherOpen] = useState(false); + const [text, setText] = useState(""); if (!reserved?.length) return null; - return
{reserved.map((c) => ( - - ))}
; + return ( +
+
+ {reserved.map((c) => ( + + ))} +
+ {otherOpen && ( +
+