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

✨ - feat: radio implemented #103

Merged
merged 3 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions src/components/form/checkbox/checkbox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const CheckboxComponent: Story = {
args: {
children: "Click me!",
name: "input",
value: "on",
},
argTypes: FORM_TEST_ARG_TYPES,
decorators: [FORM_TEST_DECORATOR],
Expand Down
4 changes: 4 additions & 0 deletions src/components/form/choicefield/choicefield.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";

import { CheckboxGroup, CheckboxGroupProps } from "../checkbox/checkboxgroup";
import { RadioGroup, RadioGroupProps } from "../radio";
import { Select, SelectProps } from "../select";

export type ChoiceFieldProps<
Expand Down Expand Up @@ -58,5 +59,8 @@ export const ChoiceField: React.FC<ChoiceFieldProps> = ({ type, ...props }) => {
if (type === "checkbox") {
return <CheckboxGroup {...(props as CheckboxGroupProps)}></CheckboxGroup>;
}
if (type === "radio") {
return <RadioGroup {...(props as RadioGroupProps)}></RadioGroup>;
}
return <Select {...(props as SelectProps)} />;
};
27 changes: 26 additions & 1 deletion src/components/form/form/form.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ export const FormComponent: Story = {
{ label: "Science", value: "science" },
],
},
{
label: "Receive newsletter",
name: "subscribe_newsletter",
type: "radio",
required: true,
options: [
{ label: "Yes", value: "yes" },
{ label: "No", value: "no" },
],
},
],
validate: validateForm,
validateOnChange: true,
Expand Down Expand Up @@ -123,7 +133,12 @@ export const TypedResults: Story = {
{
label: "Receive newsletter",
name: "subscribe_newsletter",
type: "checkbox",
type: "radio",
required: true,
options: [
{ label: "Yes", value: "yes" },
{ label: "No", value: "no" },
],
},
],
useTypedResults: true,
Expand Down Expand Up @@ -189,6 +204,16 @@ export const UsageWithFormik: Story = {
{ label: "Science", value: "science" },
],
},
{
label: "Receive newsletter",
name: "subscribe_newsletter",
type: "radio",
required: true,
options: [
svenvandescheur marked this conversation as resolved.
Show resolved Hide resolved
{ label: "Yes", value: "yes" },
{ label: "No", value: "no" },
],
},
],
},
render: (args) => {
Expand Down
10 changes: 9 additions & 1 deletion src/components/form/formcontrol/formcontrol.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import {
isChoiceField,
isDatePicker,
isInput,
isRadio,
isRadioGroup,
} from "../../../lib/form/typeguards";
import { Checkbox } from "../checkbox";
import { ChoiceField } from "../choicefield";
import { DatePicker } from "../datepicker";
import { ErrorMessage } from "../errormessage";
import { Input, InputProps } from "../input";
import { Label } from "../label";
import { Radio } from "../radio";
import "./formcontrol.scss";

export type FormControlProps = FormField & {
Expand Down Expand Up @@ -47,7 +50,8 @@ export const FormControl: React.FC<FormControlProps> = ({
const id = useId();
const _id = props.id || id;
// Keep in sync with CheckboxGroup, possibly add constant?
const htmlFor = isCheckboxGroup(props) ? `${_id}-choice-0` : _id;
const htmlFor =
isCheckboxGroup(props) || isRadioGroup(props) ? `${_id}-choice-0` : _id;
const idError = `${id}_error`;

return (
Expand Down Expand Up @@ -88,6 +92,10 @@ export const FormWidget: React.FC<FormField> = ({ ...props }) => {
return <Checkbox {...props} />;
}

if (isRadio(props)) {
return <Radio {...props} />;
}

if (isDatePicker(props)) {
return <DatePicker {...props} />;
}
Expand Down
3 changes: 2 additions & 1 deletion src/components/form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ export * from "./checkbox";
export * from "./choicefield";
export * from "./datepicker";
export * from "./errormessage";
export * from "./errors";
export * from "./form";
export * from "./formcontrol";
export * from "./input";
export * from "./label";
export * from "./radio";
export * from "./select";
export * from "./errors";
22 changes: 21 additions & 1 deletion src/components/form/input/input.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
width: auto;
}

&[type="checkbox"] {
&[type="checkbox"],
&[type="radio"] {
cursor: pointer;
height: 16px;
max-width: 16px;
Expand All @@ -30,6 +31,9 @@
&:checked {
border-color: var(--page-color-primary);
}
}

&[type="checkbox"] {
&:checked:before {
background-color: var(--page-color-primary);
mask-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2217%22%20height%3D%2216%22%20viewBox%3D%220%200%2017%2016%22%20fill%3D%22none%22%3E%3Cpath%20d%3D%22m3.594%208%204.019%204.019a1%201%200%200%200%201.601-.26L13.594%203%22%20stroke%3D%22%23341A90%22%20stroke-width%3D%221.5%22%20stroke-linecap%3D%22round%22%2F%3E%3C%2Fsvg%3E);
Expand All @@ -43,6 +47,22 @@
}
}

&[type="radio"] {
border-radius: 50%;

&:checked:before {
background-color: var(--page-color-primary);
border-radius: 50%;
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 8px;
height: 8px;
}
}

&[type="color"] {
min-height: 38px;
overflow: hidden;
Expand Down
16 changes: 16 additions & 0 deletions src/components/form/input/input.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ const meta: Meta<typeof Input> = {
clearable = false;
testValue = args.value || "on";
break;
case "radio":
input = await canvas.getByRole("radio");
clearable = false;
testValue = args.value || "on";
break;
case "number":
input = await canvas.getByRole("spinbutton");
break;
Expand Down Expand Up @@ -84,6 +89,17 @@ export const InputTypeCheckbox: Story = {
args: {
name: "input",
type: "checkbox",
value: "true",
},
argTypes: FORM_TEST_ARG_TYPES,
decorators: [FORM_TEST_DECORATOR],
};

export const InputTypeRadio: Story = {
args: {
name: "input",
type: "radio",
value: "true",
},
argTypes: FORM_TEST_ARG_TYPES,
decorators: [FORM_TEST_DECORATOR],
Expand Down
15 changes: 4 additions & 11 deletions src/components/form/input/input.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import clsx from "clsx";
import React, { useEffect, useState } from "react";

import { StackCtx } from "../../stackctx";
Expand Down Expand Up @@ -75,20 +74,14 @@ export const Input: React.FC<InputProps> = ({
// For checkboxes, this is problematic as it loses the "on" | "off" values.
// This conditionalizes the presence of the "value" props.
const valueProps =
type.toLowerCase() === "checkbox"
? typeof value === "undefined"
? {}
: {
value: value,
}
: {
value: valueState,
};
type.toLowerCase() === "checkbox" || type.toLowerCase() === "radio"
? { checked: props.checked, value: valueState }
svenvandescheur marked this conversation as resolved.
Show resolved Hide resolved
: { value: valueState };

const input = (
<input
ref={inputRef}
className={clsx("mykn-input", `mykn-input--variant-${variant}`)}
className={`mykn-input mykn-input--variant-${variant}`}
type={type}
onChange={_onChange}
aria-label={label || undefined}
Expand Down
2 changes: 2 additions & 0 deletions src/components/form/radio/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./radio";
export * from "./radiogroup";
6 changes: 6 additions & 0 deletions src/components/form/radio/radio.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.mykn-radio {
display: flex;
align-items: center;
height: var(--typography-line-height-body-s);
gap: var(--spacing-h);
}
37 changes: 37 additions & 0 deletions src/components/form/radio/radio.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { Meta, StoryObj } from "@storybook/react";
import { expect, userEvent, within } from "@storybook/test";

import { FORM_TEST_DECORATOR } from "../.storybook/decorators";
import { Radio } from "./radio";

const meta: Meta<typeof Radio> = {
title: "Form/Radio",
component: Radio,
};

export default meta;
type Story = StoryObj<typeof meta>;

const FORM_TEST_ARG_TYPES = {
onChange: { action: "onChange" },
};

export const RadioComponent: Story = {
args: {
children: "Click me!",
name: "input",
value: "on",
},
argTypes: FORM_TEST_ARG_TYPES,
decorators: [FORM_TEST_DECORATOR],
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const pre = await canvas.findByRole("log");
const input = canvas.getByLabelText(args.children);

// On
await userEvent.click(input);
const data = JSON.parse(pre?.textContent || "{}");
await expect(data.input).toBe(args.value || "on");
},
};
37 changes: 37 additions & 0 deletions src/components/form/radio/radio.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, { useId } from "react";

import { Input, InputProps } from "../input";
import { Label } from "../label";
import "./radio.scss";

export type RadioProps = InputProps & {
/** The (optional) label to display next to the checkbox. */
children?: React.ReactNode;
};

/**
* Radio component, similar to Input with type set to "radio" but allows children to be passed
* to show an inline label.
* @param children
* @param props
* @constructor
*/
export const Radio: React.FC<RadioProps> = ({
children,
value,
checked,
...props
}) => {
const id = useId();
const _id = props.id || id;
return (
<div className="mykn-radio">
<Input id={_id} {...props} type="radio" value={value} checked={checked} />
{children && (
<Label bold={false} htmlFor={_id}>
{children}
</Label>
)}
</div>
);
};
58 changes: 58 additions & 0 deletions src/components/form/radio/radiogroup.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { Meta, StoryObj } from "@storybook/react";
import { expect, userEvent, within } from "@storybook/test";

import { FORM_TEST_DECORATOR } from "../.storybook/decorators";
import { RadioGroup } from "./radiogroup";

const meta: Meta<typeof RadioGroup> = {
title: "Form/Radio",
component: RadioGroup,
};
export default meta;
type Story = StoryObj<typeof meta>;

const FORM_TEST_ARG_TYPES = {
onChange: { action: "onChange" },
};

export const RadioGroupComponent: Story = {
args: {
name: "school_year",
options: [
{ label: "Freshman", selected: true },
{ label: "Sophomore" },
{ label: "Junior" },
{ label: "Senior" },
{ label: "Graduate" },
],
},
argTypes: FORM_TEST_ARG_TYPES,
decorators: [FORM_TEST_DECORATOR],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const pre = await canvas.findByRole("log");
const inputs = canvas.getAllByRole("radio");
let data;

await inputs.reduce(
(promises, input) =>
promises.then(() => userEvent.click(input, { delay: 10 })),
Promise.resolve(),
);

data = JSON.parse(pre?.textContent || "{}");

await expect(data.school_year[0]).toEqual("Graduate");

await inputs
.filter((_, i) => i % 2 !== 0)
.reduce(
(promises, input) =>
promises.then(() => userEvent.click(input, { delay: 10 })),
Promise.resolve(),
);

data = JSON.parse(pre?.textContent || "{}");
await expect(data.school_year[0]).toEqual("Senior");
},
};
Loading