Skip to content

Commit

Permalink
Handle checking and unchecking correctly in grouped scenario
Browse files Browse the repository at this point in the history
  • Loading branch information
jamdelion committed Jan 3, 2025
1 parent 2d0cb1a commit 6ba61dc
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import Box from "@mui/material/Box";
import Grid from "@mui/material/Grid";
import Typography from "@mui/material/Typography";
import { Group } from "@planx/components/Checklist/model";
import { FormikProps } from "formik";
import { partition } from "lodash";
import React from "react";
import { ExpandableListItem } from "ui/public/ExpandableList";
import FormWrapper from "ui/public/FormWrapper";
import ChecklistItem from "ui/shared/ChecklistItem/ChecklistItem";

import { Option } from "../../../../shared";
import { toggleNonExclusiveCheckbox } from "../../helpers";
import { useExclusiveOption } from "../../hooks/useExclusiveOption";
import { ExclusiveChecklistItem } from "../ExclusiveChecklistItem";
import { useExclusiveOptionInGroupedChecklists } from "../../hooks/useExclusiveOption";

interface Props {
group: Group<Option>;
index: number;
isExpanded: boolean;
formik: FormikProps<{
checked: Array<string>;
checked: Record<string, Array<string>>;
}>;
setCheckedFieldValue: (optionIds: string[]) => void;
toggleGroup: (index: number) => void;
}

Expand All @@ -27,36 +28,41 @@ export const ChecklistOptionGroup = ({
index,
isExpanded,
formik,
setCheckedFieldValue,
toggleGroup,
}: Props) => {
const [exclusiveOptions, nonExclusiveOptions]: Option[][] = partition(
group.children,
(option: Option) => option.data.exclusive
);

const { exclusiveOrOption, toggleExclusiveCheckbox } = useExclusiveOption(
exclusiveOptions,
formik
);
const { exclusiveOrOption, toggleExclusiveCheckbox } =
useExclusiveOptionInGroupedChecklists(
exclusiveOptions,
group.title,
formik
);

const changeCheckbox = (id: string) => () => {
const currentCheckedIds = formik.values.checked;
const allCheckedIds = formik.values.checked;
const currentCheckedIds = allCheckedIds[group.title];

const currentCheckboxIsExclusiveOption =
exclusiveOrOption && id === exclusiveOrOption.id;

if (currentCheckboxIsExclusiveOption) {
const newCheckedIds = toggleExclusiveCheckbox(id);
setCheckedFieldValue(newCheckedIds);
formik.setFieldValue("checked", newCheckedIds);
return;
}
const newCheckedIds = toggleNonExclusiveCheckbox(
id,
currentCheckedIds,
exclusiveOrOption
);
setCheckedFieldValue(newCheckedIds);

allCheckedIds[group.title] = newCheckedIds;

formik.setFieldValue("checked", allCheckedIds);
};

return (
Expand All @@ -81,15 +87,26 @@ export const ChecklistOptionGroup = ({
key={option.data.text}
label={option.data.text}
id={option.id}
checked={formik.values.checked.includes(option.id)}
checked={formik.values.checked[group.title].includes(option.id)}
/>
))}
{exclusiveOrOption && (
<ExclusiveChecklistItem
exclusiveOrOption={exclusiveOrOption}
changeCheckbox={changeCheckbox}
formik={formik}
/>
// Exclusive or option
<FormWrapper key={exclusiveOrOption.id}>
<Grid item xs={12} key={exclusiveOrOption.data.text}>
<Typography width={36} display="flex" justifyContent="center">
or
</Typography>
<ChecklistItem
onChange={changeCheckbox(exclusiveOrOption.id)}
label={exclusiveOrOption.data.text}
id={exclusiveOrOption.id}
checked={formik.values.checked[group.title].includes(
exclusiveOrOption.id
)}
/>
</Grid>
</FormWrapper>
)}
</Box>
</ExpandableListItem>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import Grid from "@mui/material/Grid";
import { visuallyHidden } from "@mui/utils";
import { checklistValidationSchema } from "@planx/components/Checklist/model";
import {
getLayout,
groupedChecklistValidationSchema,
} from "@planx/components/Checklist/model";
import Card from "@planx/components/shared/Preview/Card";
import { CardHeader } from "@planx/components/shared/Preview/CardHeader/CardHeader";
import { getIn, useFormik } from "formik";
Expand All @@ -10,7 +13,6 @@ import ErrorWrapper from "ui/shared/ErrorWrapper";
import { object } from "yup";

import { PublicChecklistProps } from "../../../types";
import { useSortedOptions } from "../../hooks/useSortedOptions";
import { ChecklistLayout } from "../VisibleChecklist";
import { GroupedChecklistOptions } from "./GroupedChecklistOptions";

Expand All @@ -29,25 +31,34 @@ export const GroupedChecklist: React.FC<PublicChecklistProps> = (props) => {
id,
} = props;

const formik = useFormik<{ checked: Array<string> }>({
const formik = useFormik<{ checked: Record<string, Array<string>> }>({
initialValues: {
checked: previouslySubmittedData?.answers || [],
// e.g. { 'Section 1': [], 'Section 2': [ 'S2_Option2' ] }
checked:
groupedOptions?.reduce(
(acc, group) => ({
...acc,
[group.title]:
previouslySubmittedData?.answers?.filter((id) =>
group.children.some((item) => item.id === id)
) || [],
}),
{}
) || {},
},
onSubmit: (values) => {
handleSubmit?.({ answers: values.checked });
const flattenedCheckedIds = Object.values(values.checked).flat();
handleSubmit?.({ answers: flattenedCheckedIds });
},
validateOnBlur: false,
validateOnChange: false,
validationSchema: object({
checked: checklistValidationSchema(props),
checked: groupedChecklistValidationSchema(props),
}),
});

const { setCheckedFieldValue, layout } = useSortedOptions(
options,
groupedOptions,
formik
);
// TODO: do we need useSortedOptions ?
const layout = getLayout({ options, groupedOptions });

return (
<Card handleSubmit={formik.handleSubmit} isValid>
Expand All @@ -71,7 +82,6 @@ export const GroupedChecklist: React.FC<PublicChecklistProps> = (props) => {
<GroupedChecklistOptions
groupedOptions={groupedOptions}
previouslySubmittedData={previouslySubmittedData}
setCheckedFieldValue={setCheckedFieldValue}
formik={formik}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,12 @@ import { ChecklistOptionGroup } from "./ChecklistOptionGroup";
interface GroupedChecklistOptionsProps {
groupedOptions: Group<Option>[];
previouslySubmittedData: Store.UserData | undefined;
setCheckedFieldValue: (optionIds: string[]) => void;
formik: FormikProps<{ checked: Array<string> }>;
formik: FormikProps<{ checked: Record<string, Array<string>> }>;
}

export const GroupedChecklistOptions = ({
groupedOptions,
previouslySubmittedData,
setCheckedFieldValue,
formik,
}: GroupedChecklistOptionsProps) => {
const { expandedGroups, toggleGroup } = useExpandedGroups(
Expand All @@ -42,7 +40,6 @@ export const GroupedChecklistOptions = ({
isExpanded={isExpanded}
index={index}
formik={formik}
setCheckedFieldValue={setCheckedFieldValue}
toggleGroup={toggleGroup}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const useExclusiveOption = (
const exclusiveOptionIsChecked =
exclusiveOrOption && formik.values.checked.includes(exclusiveOrOption.id);

const toggleExclusiveCheckbox = (checkboxId: string) => {
const toggleExclusiveCheckbox = (checkboxId: string): Array<string> => {
return exclusiveOptionIsChecked ? [] : [checkboxId];
};

Expand All @@ -21,3 +21,38 @@ export const useExclusiveOption = (
toggleExclusiveCheckbox,
};
};

export const useExclusiveOptionInGroupedChecklists = (
exclusiveOptions: Option[],
checklistGroupTitle: string,
formik: FormikProps<{ checked: Record<string, Array<string>> }>
) => {
const [exclusiveOrOption] = exclusiveOptions;

const exclusiveOptionIsChecked =
exclusiveOrOption &&
formik.values.checked[checklistGroupTitle] &&
formik.values.checked[checklistGroupTitle].includes(exclusiveOrOption.id);

const toggleExclusiveCheckbox = (
checkboxId: string
): Record<string, Array<string>> => {
const newCheckedIds = formik.values.checked;
if (exclusiveOptionIsChecked) {
if (checklistGroupTitle in formik.values.checked) {
// empty the array in this checklist section only
newCheckedIds[checklistGroupTitle] = [];
}
} else {
// add in the checkboxId at this index
newCheckedIds[checklistGroupTitle] = [checkboxId];
}
return newCheckedIds;
};

return {
exclusiveOrOption,
exclusiveOptionIsChecked,
toggleExclusiveCheckbox,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -72,23 +72,34 @@ describe("Checklist Component - Grouped Layout", () => {
/>
);

// user presses exclusive option in section 1
await user.click(screen.getByText("Section 1"));

const exclusiveOptionInSection1 = screen.getByText("S1 Option1");
const nonExclusiveOptionInSection1 = screen.getByText("S1 Option2");

const exclusiveOptionInSection1 = screen.getByLabelText("S1 Option1");
const nonExclusiveOptionInSection1 = screen.getByLabelText("S1 Option2");
await user.click(exclusiveOptionInSection1);

expect(exclusiveOptionInSection1).toHaveAttribute("checked");

// user presses non-exclusive option in section 1, exclusive option should uncheck.
await user.click(nonExclusiveOptionInSection1);
expect(exclusiveOptionInSection1).not.toHaveAttribute("checked");
expect(nonExclusiveOptionInSection1).toHaveAttribute("checked");

// user presses exclusive option in section 3
await user.click(screen.getByText("Section 3"));
const exclusiveOptionInSection3 = screen.getByLabelText("S3 Option2");
await user.click(exclusiveOptionInSection3);

// options in other checklists should not be affected
expect(exclusiveOptionInSection3).toHaveAttribute("checked");
expect(nonExclusiveOptionInSection1).toHaveAttribute("checked");

// user presses two non-exclusive options in this section
await user.click(screen.getByText("S3 Option1"));
await user.click(screen.getByText("S3 Option3"));

expect(exclusiveOptionInSection3).not.toHaveAttribute("checked");

await user.click(screen.getByTestId("continue-button"));

expect(handleSubmit).toHaveBeenCalledWith({
Expand Down Expand Up @@ -161,5 +172,12 @@ describe("Checklist Component - Grouped Layout", () => {
expect(handleSubmit).toHaveBeenCalledWith({
answers: ["S2_Option1", "S2_Option2"],
});
// expect(handleSubmit).toHaveBeenCalledWith({
// answers: {
// "Section 1": [],
// "Section 2": ["S2_Option1", "S2_Option2"],
// "Section 3": [],
// },
// });
});
});
40 changes: 39 additions & 1 deletion editor.planx.uk/src/@planx/components/Checklist/model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { array } from "yup";
import { array, object, string } from "yup";

import { BaseNodeData, Option } from "../shared";
import { ChecklistLayout } from "./Public/components/VisibleChecklist";
Expand Down Expand Up @@ -126,3 +126,41 @@ export const checklistValidationSchema = ({
},
});
};

export const groupedChecklistValidationSchema = ({
allRequired,
options,
groupedOptions,
}: Checklist) => {
const flatOptions = getFlatOptions({ options, groupedOptions });

return object()
.shape(
Object.fromEntries(
groupedOptions?.map((group) => [group.title, array().of(string())]) ||
[]
)
)
.required()
.test({
name: "atLeastOneChecked",
message: "Select at least one option",
test: (value: Record<string, string[]>) => {
const arrayOfArrays = Object.values(value || {});
return arrayOfArrays.some((arr) => arr.length > 0);
},
})
.test({
name: "notAllChecked",
message: "All options must be checked",
test: (checked?: Record<string, string[]>) => {
if (!allRequired) {
return true;
}
const allChecked =
checked &&
Object.values(checked).flat().length === flatOptions.length;
return Boolean(allChecked);
},
});
};

0 comments on commit 6ba61dc

Please sign in to comment.