Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Radio Element #60

Merged
merged 5 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions services/ui-src/src/components/fields/RadioField.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { RadioField } from "components";
import { useFormContext } from "react-hook-form";
import { PageElement } from "types";
import { testA11y } from "utils/testing/commonTests";

const mockTrigger = jest.fn();
const mockSetValue = jest.fn();
const mockRhfMethods = {
register: () => {},
setValue: mockSetValue,
getValues: jest.fn(),
trigger: mockTrigger,
};
const mockUseFormContext = useFormContext as unknown as jest.Mock<
typeof useFormContext
>;
jest.mock("react-hook-form", () => ({
useFormContext: jest.fn(() => mockRhfMethods),
}));
const mockGetValues = (returnValue: any) =>
mockUseFormContext.mockImplementation((): any => ({
...mockRhfMethods,
getValues: jest.fn().mockReturnValueOnce([]).mockReturnValue(returnValue),
}));

const mockRadioElement = {
type: RadioField,
label: "mock label",
value: [
{
label: "Choice 1",
value: "A",
checked: false,
},
{
label: "Choice 2",
value: "B",
checked: false,
},
{
label: "Choice 3",
value: "C",
checked: false,
},
],
};

const RadioFieldComponent = (
<div data-testid="test-radio-list">
<RadioField
element={mockRadioElement as unknown as PageElement}
index={0}
formkey="elements.0"
/>
</div>
);

describe("<RadioField />", () => {
test("RadioField renders as Radio", () => {
mockGetValues(undefined);
render(RadioFieldComponent);
expect(screen.getByText("Choice 1")).toBeVisible();
expect(screen.getByTestId("test-radio-list")).toBeVisible();
});

test("RadioField allows checking radio choices", async () => {
mockGetValues(undefined);
render(RadioFieldComponent);
const firstRadio = screen.getByLabelText("Choice 1") as HTMLInputElement;
await userEvent.click(firstRadio);
expect(mockSetValue).toHaveBeenCalledWith("elements.0.answer", "A", {
shouldValidate: true,
});
});

testA11y(RadioFieldComponent, () => {
mockGetValues(undefined);
});
});
60 changes: 60 additions & 0 deletions services/ui-src/src/components/fields/RadioField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, { useState, useEffect } from "react";
import { Box } from "@chakra-ui/react";
import { PageElementProps } from "components/report/Elements";
import { useFormContext } from "react-hook-form";
import { ChoiceTemplate, RadioTemplate } from "types";
import { parseCustomHtml } from "utils";
import { ChoiceList as CmsdsChoiceList } from "@cmsgov/design-system";

export const RadioField = (props: PageElementProps) => {
const radio = props.element as RadioTemplate;

const defaultValue = radio.value ?? [];
const [displayValue, setDisplayValue] =
useState<ChoiceTemplate[]>(defaultValue);

// get form context and register field
const form = useFormContext();
const key = `${props.formkey}.answer`;
useEffect(() => {
form.register(key);
}, []);

const onChangeHandler = async (
event: React.ChangeEvent<HTMLInputElement>
) => {
const { name, value } = event.target;
const newValues = displayValue.map((choice) => {
choice.checked = choice.value === value;
return choice;
});
setDisplayValue(newValues);
form.setValue(name, value, { shouldValidate: true });
};

const onBlurHandler = async (event: React.ChangeEvent<HTMLInputElement>) => {
form.setValue(key, event.target.value);
};

// prepare error message, hint, and classes
const formErrorState = form?.formState?.errors;
const errorMessage = formErrorState?.[key]?.message;
const parsedHint = radio.helperText && parseCustomHtml(radio.helperText);
const labelText = radio.label;

return (
<Box>
<CmsdsChoiceList
name={key}
type={"radio"}
label={labelText || ""}
choices={displayValue}
hint={parsedHint}
errorMessage={errorMessage}
onChange={onChangeHandler}
onComponentBlur={onBlurHandler}
{...props}
/>
</Box>
);
};
1 change: 1 addition & 0 deletions services/ui-src/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export { TemplateCard } from "./cards/TemplateCard";
export { ExportedReportBanner } from "./export/ExportedReportBanner";
export { ExportedReportWrapper } from "./export/ExportedReportWrapper";
// fields
export { RadioField } from "./fields/RadioField";
export { TextField } from "./fields/TextField";
export { DateField } from "./fields/DateField";
// forms
Expand Down
29 changes: 1 addition & 28 deletions services/ui-src/src/components/report/Elements.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
import {
Button,
FormLabel,
Heading,
Radio,
RadioGroup,
Stack,
Image,
Text,
} from "@chakra-ui/react";
import { Button, Heading, Stack, Image, Text } from "@chakra-ui/react";
import { useStore } from "utils";
import {
HeaderTemplate,
SubHeaderTemplate,
ParagraphTemplate,
AccordionTemplate,
RadioTemplate,
ButtonLinkTemplate,
PageElement,
} from "../../types/report";
Expand Down Expand Up @@ -67,23 +57,6 @@ export const accordionElement = (props: PageElementProps) => {
);
};

export const radioElement = (props: PageElementProps) => {
const radio = props.element as RadioTemplate;

return (
<RadioGroup>
<FormLabel fontWeight="bold">{radio.label}</FormLabel>
<Stack direction="column">
{radio.value.map((child, index) => (
<Radio key={index} value={child.value}>
{child.label}
</Radio>
))}
</Stack>
</RadioGroup>
);
};

export const buttonLinkElement = (props: PageElementProps) => {
const button = props.element as ButtonLinkTemplate;
const { setCurrentPageId } = useStore();
Expand Down
6 changes: 2 additions & 4 deletions services/ui-src/src/components/report/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@ import {
subHeaderElement,
paragraphElement,
accordionElement,
radioElement,
buttonLinkElement,
} from "./Elements";
import { TextField } from "../fields/TextField";
import { assertExhaustive, ElementType, PageElement } from "../../types/report";
import { MeasureTableElement } from "./MeasureTable";
import { StatusTableElement } from "./StatusTable";
import { DateField } from "../fields/DateField";
import { TextField, DateField, RadioField } from "components";

interface Props {
elements: PageElement[];
Expand All @@ -34,7 +32,7 @@ export const Page = ({ elements }: Props) => {
case ElementType.Accordion:
return accordionElement;
case ElementType.Radio:
return radioElement;
return RadioField;
case ElementType.ButtonLink:
return buttonLinkElement;
case ElementType.MeasureTable:
Expand Down
3 changes: 3 additions & 0 deletions services/ui-src/src/types/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ export type RadioTemplate = {
type: ElementType.Radio;
label: string;
value: ChoiceTemplate[];
helperText?: string;
answer?: string;
};

export type ButtonLinkTemplate = {
Expand All @@ -191,6 +193,7 @@ export type ButtonLinkTemplate = {
export type ChoiceTemplate = {
label: string;
value: string;
checked?: boolean;
};

export enum DeliverySystem {
Expand Down
Loading