Skip to content

Commit

Permalink
feat: List component - basic model, types & UI (#3197)
Browse files Browse the repository at this point in the history
  • Loading branch information
DafyddLlyr authored and RODO94 committed Jun 4, 2024
1 parent 509662b commit 7f8a15d
Show file tree
Hide file tree
Showing 11 changed files with 559 additions and 44 deletions.
85 changes: 82 additions & 3 deletions editor.planx.uk/src/@planx/components/List/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
import MenuItem from "@mui/material/MenuItem";
import { ComponentType as TYPES } from "@opensystemslab/planx-core/types";
import { useFormik } from "formik";
import React from "react";
import { FeaturePlaceholder } from "ui/editor/FeaturePlaceholder";
import ModalSection from "ui/editor/ModalSection";
import ModalSectionContent from "ui/editor/ModalSectionContent";
import RichTextInput from "ui/editor/RichTextInput";
import SelectInput from "ui/editor/SelectInput";
import Input from "ui/shared/Input";
import InputRow from "ui/shared/InputRow";
import InputRowItem from "ui/shared/InputRowItem";
import InputRowLabel from "ui/shared/InputRowLabel";

import { EditorProps } from "../ui";
import { EditorProps, ICONS, InternalNotes, MoreInformation } from "../ui";
import { List, parseContent } from "./model";
import { Zoo } from "./schemas/Zoo";

type Props = EditorProps<TYPES.List, List>;

export const SCHEMAS = [
{ name: "Zoo", schema: Zoo },
// TODO: Residential units
// TODO: Residential units (GLA)
];

function ListComponent(props: Props) {
const formik = useFormik({
initialValues: parseContent(props.node?.data),
Expand All @@ -21,7 +36,71 @@ function ListComponent(props: Props) {

return (
<form onSubmit={formik.handleSubmit} id="modal">
<FeaturePlaceholder title="Under development" />
<ModalSection>
<ModalSectionContent title="List" Icon={ICONS[TYPES.List]}>
<InputRow>
<Input
format="large"
name="title"
value={formik.values.title}
placeholder="Title"
onChange={formik.handleChange}
/>
</InputRow>
<InputRow>
<RichTextInput
placeholder="Description"
name="description"
value={formik.values.description}
onChange={formik.handleChange}
/>
</InputRow>
<InputRow>
<Input
format="data"
name="fn"
value={formik.values.fn}
placeholder="Data Field"
onChange={formik.handleChange}
/>
</InputRow>
<InputRow>
<InputRowLabel>Schema</InputRowLabel>
<InputRowItem>
<SelectInput
value={formik.values.schemaName}
onChange={(e) => {
formik.setFieldValue("schemaName", e.target.value);
formik.setFieldValue(
"schema",
SCHEMAS.find(
({ name }) => name === (e.target.value as string),
)?.schema,
);
}}
>
{SCHEMAS.map(({ name }) => (
<MenuItem key={name} value={name}>
{name}
</MenuItem>
))}
</SelectInput>
</InputRowItem>
</InputRow>
</ModalSectionContent>
</ModalSection>
<MoreInformation
changeField={formik.handleChange}
definitionImg={formik.values.definitionImg}
howMeasured={formik.values.howMeasured}
policyRef={formik.values.policyRef}
info={formik.values.info}
/>
<InternalNotes
name="notes"
value={formik.values.notes}
onChange={formik.handleChange}
/>
</form>
);
}
Expand Down
10 changes: 0 additions & 10 deletions editor.planx.uk/src/@planx/components/List/Public.test.tsx

This file was deleted.

27 changes: 0 additions & 27 deletions editor.planx.uk/src/@planx/components/List/Public.tsx

This file was deleted.

137 changes: 137 additions & 0 deletions editor.planx.uk/src/@planx/components/List/Public/Fields.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import Box from "@mui/material/Box";
import FormControl from "@mui/material/FormControl";
import FormLabel from "@mui/material/FormLabel";
import MenuItem from "@mui/material/MenuItem";
import RadioGroup from "@mui/material/RadioGroup";
import React from "react";
import SelectInput from "ui/editor/SelectInput";
import InputLabel from "ui/public/InputLabel";
import Input from "ui/shared/Input";
import InputRowLabel from "ui/shared/InputRowLabel";

import { DESCRIPTION_TEXT } from "../../shared/constants";
import BasicRadio from "../../shared/Radio/BasicRadio";
import type { NumberField, QuestionField, TextField } from "../model";

type Props<T> = T & { id: string };

export const TextFieldInput: React.FC<Props<TextField>> = ({
id,
data,
required,
}) => (
<InputLabel label={data.title} htmlFor={id}>
<Input
type={((type) => {
if (type === "email") return "email";
else if (type === "phone") return "tel";
return "text";
})(data.type)}
multiline={data.type && ["long", "extraLong"].includes(data.type)}
bordered
id={id}
rows={
data.type && ["long", "extraLong"].includes(data.type) ? 5 : undefined
}
name="text"
required={required}
inputProps={{
"aria-describedby": [
data.description ? DESCRIPTION_TEXT : "",
// TODO: When handling errors, revisit this
// formik.errors.text ? `${ERROR_MESSAGE}-${inputFieldId}` : "",
]
.filter(Boolean)
.join(" "),
}}
/>
</InputLabel>
);

export const NumberFieldInput: React.FC<Props<NumberField>> = ({
id,
data,
required,
}) => (
<InputLabel label={data.title} htmlFor={id}>
<Box sx={{ display: "flex", alignItems: "baseline" }}>
<Input
required={required}
bordered
name="value"
type="number"
// value={formik.values.value}
// onChange={formik.handleChange}
// errorMessage={formik.errors.value as string}
inputProps={{
"aria-describedby": [
data.description ? DESCRIPTION_TEXT : "",
// formik.errors.value ? `${ERROR_MESSAGE}-${props.id}` : "",
]
.filter(Boolean)
.join(" "),
}}
id={id}
/>
{data.units && <InputRowLabel>{data.units}</InputRowLabel>}
</Box>
</InputLabel>
);

export const RadioFieldInput: React.FC<Props<QuestionField>> = ({
id,
data,
}) => (
<FormControl sx={{ width: "100%" }} component="fieldset">
<FormLabel
component="legend"
id={`radio-buttons-group-label-${id}`}
sx={(theme) => ({
color: theme.palette.text.primary,
"&.Mui-focused": {
color: theme.palette.text.primary,
},
})}
>
{data.title}
</FormLabel>
{/* <ErrorWrapper id={props.id} error={formik.errors.selected?.id}> */}
<RadioGroup
aria-labelledby={`radio-buttons-group-label-${id}`}
name={`radio-buttons-group-${id}`}
sx={{ p: 1 }}
// value={formik.values.selected.id}
>
{data.options.map(({ id, data }) => (
<BasicRadio
key={id}
id={id}
title={data.text}
onChange={() => console.log("change radio")}
/>
))}
</RadioGroup>
{/* </ErrorWrapper> */}
</FormControl>
);

export const SelectFieldInput: React.FC<Props<QuestionField>> = ({
id,
data,
required,
}) => (
<InputLabel label={data.title} id={`select-label-${id}`}>
<SelectInput
bordered
required={required}
title={data.title}
labelId={`select-label-${id}`}
>
{data.options.map((option) => (
<MenuItem key={option.id} value={option.data.text}>
{option.data.text}
</MenuItem>
))}
</SelectInput>
</InputLabel>
);
116 changes: 116 additions & 0 deletions editor.planx.uk/src/@planx/components/List/Public/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React from "react";
import { axe, setup } from "testUtils";

import ListComponent, { Props } from "../Public";
import { Zoo } from "../schemas/Zoo";

const mockProps: Props = {
fn: "mock",
schema: Zoo,
schemaName: "Zoo",
title: "Mock Title",
description: "Mock description",
};

describe("Basic UI", () => {
it("renders correctly", () => {
const { getByText } = setup(<ListComponent {...mockProps} />);

expect(getByText(/Mock Title/)).toBeInTheDocument();
expect(getByText(/Mock description/)).toBeInTheDocument();
});

it("parses provided schema to render expected form", async () => {
const { getByLabelText, getByText, user, getByRole, queryAllByRole } =
setup(<ListComponent {...mockProps} />);

// Text inputs are generated from schema...
const textInput = getByLabelText(/What's their name?/) as HTMLInputElement;
expect(textInput).toBeInTheDocument();
expect(textInput.type).toBe("text");

// Props are correctly read
const emailInput = getByLabelText(
/What's their email address?/,
) as HTMLInputElement;
expect(emailInput).toBeInTheDocument();
expect(emailInput.type).toBe("email");

// Number inputs are generated from schema
const numberInput = getByLabelText(/How old are they?/) as HTMLInputElement;
expect(numberInput).toBeInTheDocument();
expect(numberInput.type).toBe("number");

// Props are correctly read
const units = getByText(/years old/);
expect(units).toBeInTheDocument();

// Question inputs generated from schema
// Combobox displayed when number of options > 2
const selectInput = getByRole("combobox");
expect(selectInput).toBeInTheDocument();

// Open combobox
await user.click(selectInput);

// All options display
expect(getByRole("option", { name: "Small" })).toBeInTheDocument();
expect(getByRole("option", { name: "Medium" })).toBeInTheDocument();
expect(getByRole("option", { name: "Large" })).toBeInTheDocument();

// No default option selected
expect(
queryAllByRole("option", { selected: true }) as HTMLOptionElement[],
).toHaveLength(0);

// Close combobox
await user.click(selectInput);

// Radio groups displayed when number of options = 2
const radioInput = getByLabelText(/How cute are they?/);
expect(radioInput).toBeInTheDocument();

// All options display
const radioButton1 = getByLabelText("Very") as HTMLInputElement;
expect(radioButton1).toBeInTheDocument();
expect(radioButton1.type).toBe("radio");

const radioButton2 = getByLabelText("Super") as HTMLInputElement;
expect(radioButton2).toBeInTheDocument();
expect(radioButton2.type).toBe("radio");

// No default option selected
expect(radioButton1).not.toBeChecked();
expect(radioButton2).not.toBeChecked();

expect(getByText(/Save/, { selector: "button" })).toBeInTheDocument();
expect(getByText(/Cancel/, { selector: "button" })).toBeInTheDocument();
});

it("should not have any accessibility violations", async () => {
const { container } = setup(<ListComponent {...mockProps} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});

describe("Navigating back", () => {
test.todo("it pre-populates list correctly");
});

describe("Building a list", () => {
test.todo("Adding an item");
test.todo("Editing an item");
test.todo("Removing an item");
});

describe("Form validation and error handling", () => {
test.todo("Text field");
test.todo("Number field");
test.todo("Question field - select");
test.todo("Question field - radio");
});

describe("Payload generation", () => {
it.todo("generates a valid payload on submission");
});
Loading

0 comments on commit 7f8a15d

Please sign in to comment.