Skip to content

Commit

Permalink
refactor: split Checklist into sub-components and hooks (#4088)
Browse files Browse the repository at this point in the history
  • Loading branch information
jamdelion authored Dec 18, 2024
1 parent bdc3467 commit a862db2
Show file tree
Hide file tree
Showing 12 changed files with 385 additions and 248 deletions.
245 changes: 3 additions & 242 deletions editor.planx.uk/src/@planx/components/Checklist/Public/Public.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,9 @@
import Box from "@mui/material/Box";
import Grid from "@mui/material/Grid";
import Typography from "@mui/material/Typography";
import { visuallyHidden } from "@mui/utils";
import {
checklistValidationSchema,
getFlatOptions,
getLayout,
} from "@planx/components/Checklist/model";
import ImageButton from "@planx/components/shared/Buttons/ImageButton";
import Card from "@planx/components/shared/Preview/Card";
import { CardHeader } from "@planx/components/shared/Preview/CardHeader/CardHeader";
import { getIn, useFormik } from "formik";
import { partition } from "lodash";
import { useStore } from "pages/FlowEditor/lib/store";
import React, { useState } from "react";
import { ExpandableList, ExpandableListItem } from "ui/public/ExpandableList";
import FormWrapper from "ui/public/FormWrapper";
import FullWidthWrapper from "ui/public/FullWidthWrapper";
import ChecklistItem from "ui/shared/ChecklistItem/ChecklistItem";
import ErrorWrapper from "ui/shared/ErrorWrapper";
import { object } from "yup";
import React from "react";

import { Option } from "../../shared";
import { Props } from "../types";
import { AutoAnsweredChecklist } from "./AutoAnsweredChecklist";
import {
getInitialExpandedGroups,
toggleInArray,
toggleNonExclusiveCheckbox,
} from "./helpers";

export enum ChecklistLayout {
Basic,
Grouped,
Images,
}
import { AutoAnsweredChecklist } from "./components/AutoAnsweredChecklist";
import { VisibleChecklist } from "./components/VisibleChecklist";

const ChecklistComponent: React.FC<Props> = (props) => {
const autoAnswerableOptions = useStore(
Expand All @@ -56,212 +25,4 @@ const ChecklistComponent: React.FC<Props> = (props) => {
return <VisibleChecklist {...props} />;
};

const VisibleChecklist: React.FC<Props> = (props) => {
const {
description = "",
groupedOptions,
handleSubmit,
howMeasured,
info,
options,
policyRef,
text,
img,
previouslySubmittedData,
id,
} = props;

const formik = useFormik<{ checked: Array<string> }>({
initialValues: {
checked: previouslySubmittedData?.answers || [],
},
onSubmit: (values) => {
handleSubmit?.({ answers: values.checked });
},
validateOnBlur: false,
validateOnChange: false,
validationSchema: object({
checked: checklistValidationSchema(props),
}),
});

const setCheckedFieldValue = (optionIds: string[]) => {
const sortedCheckedIds = sortCheckedIds(optionIds);
formik.setFieldValue("checked", sortedCheckedIds);
};

const initialExpandedGroups = getInitialExpandedGroups(
groupedOptions,
previouslySubmittedData
);

const [expandedGroups, setExpandedGroups] = useState<Array<number>>(
initialExpandedGroups
);

const layout = getLayout({ options, groupedOptions });
const flatOptions = getFlatOptions({ options, groupedOptions });

const sortCheckedIds = (ids: string[]): string[] => {
const originalIds = flatOptions.map((cb) => cb.id);
return ids.sort((a, b) => originalIds.indexOf(a) - originalIds.indexOf(b));
};

const [exclusiveOptions, nonExclusiveOptions]: Option[][] = partition(
options,
(option) => option.data.exclusive
);

const exclusiveOrOption = exclusiveOptions[0];

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

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

const changeCheckbox = (id: string) => () => {
const currentCheckedIds = formik.values.checked;

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

if (currentCheckboxIsExclusiveOption) {
const newCheckedIds = toggleExclusiveCheckbox(id);
setCheckedFieldValue(newCheckedIds);
return;
}
const newCheckedIds = toggleNonExclusiveCheckbox(
id,
currentCheckedIds,
exclusiveOrOption
);
setCheckedFieldValue(newCheckedIds);
};

return (
<Card handleSubmit={formik.handleSubmit} isValid>
<CardHeader
title={text}
description={description}
info={info}
policyRef={policyRef}
howMeasured={howMeasured}
img={img}
/>
<FullWidthWrapper>
<ErrorWrapper error={getIn(formik.errors, "checked")} id={id}>
<Grid
container
spacing={layout === ChecklistLayout.Images ? 2 : 0}
component="fieldset"
>
<legend style={visuallyHidden}>{text}</legend>
{nonExclusiveOptions.map((option) =>
layout === ChecklistLayout.Basic ? (
<FormWrapper key={option.id}>
<Grid item xs={12} key={option.data.text}>
<ChecklistItem
onChange={changeCheckbox(option.id)}
label={option.data.text}
id={option.id}
checked={
formik.values.checked.includes(option.id) &&
!exclusiveOptionIsChecked
}
/>
</Grid>
</FormWrapper>
) : (
<Grid
item
xs={12}
sm={6}
contentWrap={4}
key={option.data.text}
>
<ImageButton
title={option.data.text}
id={option.id}
img={option.data.img}
selected={formik.values.checked.includes(option.id)}
onClick={changeCheckbox(option.id)}
checkbox
/>
</Grid>
)
)}
{exclusiveOrOption && (
<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.includes(
exclusiveOrOption.id
)}
/>
</Grid>
</FormWrapper>
)}

{groupedOptions && (
<FormWrapper>
<Grid item xs={12}>
<ExpandableList>
{groupedOptions.map((group, index) => {
const isExpanded = expandedGroups.includes(index);
return (
<ExpandableListItem
key={index}
expanded={isExpanded}
onToggle={() => {
setExpandedGroups((previous) =>
toggleInArray(index, previous)
);
}}
headingId={`group-${index}-heading`}
groupId={`group-${index}-content`}
title={group.title}
>
<Box
pt={0.5}
pb={2}
aria-labelledby={`group-${index}-heading`}
id={`group-${index}-content`}
data-testid={`group-${index}${
isExpanded ? "-expanded" : ""
}`}
>
{group.children.map((option) => (
<ChecklistItem
onChange={changeCheckbox(option.id)}
key={option.data.text}
label={option.data.text}
id={option.id}
checked={formik.values.checked.includes(
option.id
)}
/>
))}
</Box>
</ExpandableListItem>
);
})}
</ExpandableList>
</Grid>
</FormWrapper>
)}
</Grid>
</ErrorWrapper>
</FullWidthWrapper>
</Card>
);
};
export default ChecklistComponent;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useEffect } from "react";

import { Props } from "../types";
import { Props } from "../../types";

// An auto-answered Checklist won't be seen by the user, but still leaves a breadcrumb
export const AutoAnsweredChecklist: React.FC<
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Grid from "@mui/material/Grid";
import { Option } from "@planx/components/shared";
import ImageButton from "@planx/components/shared/Buttons/ImageButton";
import { FormikProps } from "formik";
import React from "react";
import FormWrapper from "ui/public/FormWrapper";
import ChecklistItem from "ui/shared/ChecklistItem/ChecklistItem";

import { ChecklistLayout } from "./VisibleChecklist";

interface Props {
nonExclusiveOptions: Option[];
layout: ChecklistLayout;
changeCheckbox: (id: string) => () => void;
formik: FormikProps<{ checked: Array<string> }>;
exclusiveOptionIsChecked: boolean;
}

export const ChecklistItems = ({
nonExclusiveOptions,
layout,
changeCheckbox,
formik,
exclusiveOptionIsChecked,
}: Props) => (
<>
{nonExclusiveOptions.map((option: Option) =>
layout === ChecklistLayout.Basic ? (
<FormWrapper key={option.id}>
<Grid item xs={12} key={option.data.text}>
<ChecklistItem
onChange={changeCheckbox(option.id)}
label={option.data.text}
id={option.id}
checked={
formik.values.checked.includes(option.id) &&
!exclusiveOptionIsChecked
}
/>
</Grid>
</FormWrapper>
) : (
<Grid item xs={12} sm={6} contentWrap={4} key={option.data.text}>
<ImageButton
title={option.data.text}
id={option.id}
img={option.data.img}
selected={formik.values.checked.includes(option.id)}
onClick={changeCheckbox(option.id)}
checkbox
/>
</Grid>
)
)}
</>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Grid from "@mui/material/Grid";
import Typography from "@mui/material/Typography";
import { FormikProps } from "formik";
import React from "react";
import FormWrapper from "ui/public/FormWrapper";
import ChecklistItem from "ui/shared/ChecklistItem/ChecklistItem";

import { Option } from "../../../shared";

export const ExclusiveChecklistItem = ({
exclusiveOrOption,
changeCheckbox,
formik,
}: {
exclusiveOrOption: Option;
changeCheckbox: (id: string) => () => void;
formik: FormikProps<{ checked: Array<string> }>;
}) => (
<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.includes(exclusiveOrOption.id)}
/>
</Grid>
</FormWrapper>
);
Loading

0 comments on commit a862db2

Please sign in to comment.