From ca88e9d8e25f24aa47ab38f787ed421c423e9a6c Mon Sep 17 00:00:00 2001 From: Giuseppe Ciotola <30926550+gciotola@users.noreply.github.com> Date: Fri, 29 May 2026 10:51:02 +0200 Subject: [PATCH] fix: preserve consumer-defined extra fields in HookedInputCheckboxGroup --- .../HookedInputCheckboxGroup.test.tsx | 108 ++++++++++++++++++ .../HookedInputCheckboxGroup.tsx | 26 ++++- 2 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 packages/app-elements/src/ui/forms/InputCheckboxGroup/HookedInputCheckboxGroup.test.tsx diff --git a/packages/app-elements/src/ui/forms/InputCheckboxGroup/HookedInputCheckboxGroup.test.tsx b/packages/app-elements/src/ui/forms/InputCheckboxGroup/HookedInputCheckboxGroup.test.tsx new file mode 100644 index 000000000..d7689b4e4 --- /dev/null +++ b/packages/app-elements/src/ui/forms/InputCheckboxGroup/HookedInputCheckboxGroup.test.tsx @@ -0,0 +1,108 @@ +import { fireEvent, render } from "@testing-library/react" +import { FormProvider, useForm, useFormContext } from "react-hook-form" +import { Text } from "#ui/atoms/Text" +import { HookedInputCheckboxGroup } from "./HookedInputCheckboxGroup" + +interface FormValues { + items: Array<{ + value: string + quantity?: number + reason?: string + }> +} + +const options = [ + { + value: "BABYBIBXA19D9D000000XXXX", + content: ( + + Gray Baby Bib with Black Logo + + ), + quantity: { + min: 1, + max: 5, + }, + }, + { + value: "BASEBHAT000000FFFFFFXXXX", + content: ( + + Black Baseball Hat with White Logo + + ), + quantity: { + min: 1, + max: 7, + }, + }, +] + +function SetReasonButton(): React.JSX.Element { + const { setValue } = useFormContext() + + return ( + + ) +} + +function ValuesPreview(): React.JSX.Element { + const { watch } = useFormContext() + + return
{JSON.stringify(watch("items"))}
+} + +function TestComponent(): React.JSX.Element { + const methods = useForm({ + defaultValues: { + items: [ + { + value: "BABYBIBXA19D9D000000XXXX", + quantity: 2, + }, + { + value: "BASEBHAT000000FFFFFFXXXX", + quantity: 3, + }, + ], + }, + }) + + return ( + + + + + + ) +} + +describe("HookedInputCheckboxGroup", () => { + test("preserves extra item fields after nested setValue and toggle", () => { + const { getAllByTestId, getByRole, getByTestId } = render() + + fireEvent.click(getByRole("button", { name: "Set reason" })) + expect(getByTestId("values").textContent).toContain('"reason":"Damaged"') + + const [, secondItem] = getAllByTestId("InputCheckboxGroupItem") + assertToBeDefined(secondItem) + + fireEvent.click(secondItem) + + const values = JSON.parse(getByTestId("values").textContent ?? "null") + expect(values).toEqual([ + { + value: "BABYBIBXA19D9D000000XXXX", + quantity: 2, + reason: "Damaged", + }, + ]) + }) +}) diff --git a/packages/app-elements/src/ui/forms/InputCheckboxGroup/HookedInputCheckboxGroup.tsx b/packages/app-elements/src/ui/forms/InputCheckboxGroup/HookedInputCheckboxGroup.tsx index dd148db8b..bc71affa2 100644 --- a/packages/app-elements/src/ui/forms/InputCheckboxGroup/HookedInputCheckboxGroup.tsx +++ b/packages/app-elements/src/ui/forms/InputCheckboxGroup/HookedInputCheckboxGroup.tsx @@ -20,7 +20,7 @@ export interface HookedInputCheckboxGroupProps export const HookedInputCheckboxGroup: React.FC< HookedInputCheckboxGroupProps > = ({ name, ...props }) => { - const { control } = useFormContext() + const { control, getValues } = useFormContext() const feedback = useValidationFeedback(name) return ( @@ -31,9 +31,29 @@ export const HookedInputCheckboxGroup: React.FC< { - field.onChange(values) + // Preserve any extra fields (e.g. `reason`) that the consumer + // stored on existing items via setValue. We need the latest form + // snapshot here because field.value can lag behind nested updates. + const currentValue = getValues(name) + const current: Array<{ value: string }> = Array.isArray( + currentValue, + ) + ? currentValue.filter( + (v): v is { value: string } => + typeof v === "object" && + v !== null && + "value" in v && + typeof (v as { value: unknown }).value === "string", + ) + : [] + field.onChange( + values.map((item) => { + const existing = current.find((v) => v.value === item.value) + return existing != null ? { ...existing, ...item } : item + }), + ) }} /> )}