From 87a1e1d2a8cd13f4ce5b2baaa7fed59ab6a63b8c Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Wed, 4 Feb 2026 13:34:22 -0700 Subject: [PATCH] Created checkbox group component --- app/components-preview/page.tsx | 66 ++++++- .../CheckboxGroup/CheckboxGroup.container.tsx | 64 +++++++ .../CheckboxGroup/CheckboxGroup.types.ts | 28 +++ .../CheckboxGroup/CheckboxGroup.view.tsx | 84 +++++++++ app/components/CheckboxGroup/index.tsx | 1 + stories/CheckboxGroup.stories.js | 172 ++++++++++++++++++ 6 files changed, 410 insertions(+), 5 deletions(-) create mode 100644 app/components/CheckboxGroup/CheckboxGroup.container.tsx create mode 100644 app/components/CheckboxGroup/CheckboxGroup.types.ts create mode 100644 app/components/CheckboxGroup/CheckboxGroup.view.tsx create mode 100644 app/components/CheckboxGroup/index.tsx create mode 100644 stories/CheckboxGroup.stories.js diff --git a/app/components-preview/page.tsx b/app/components-preview/page.tsx index 8d9df16..121d623 100644 --- a/app/components-preview/page.tsx +++ b/app/components-preview/page.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import TextInput from "../components/TextInput"; import Checkbox from "../components/Checkbox"; +import CheckboxGroup from "../components/CheckboxGroup"; import RadioGroup from "../components/RadioGroup"; export default function ComponentsPreview() { @@ -11,6 +12,7 @@ export default function ComponentsPreview() { const [errorInputValue, setErrorInputValue] = useState(""); const [standardCheckbox, setStandardCheckbox] = useState(false); const [inverseCheckbox, setInverseCheckbox] = useState(false); + const [checkboxGroupValues, setCheckboxGroupValues] = useState([]); const [radioValue, setRadioValue] = useState(""); return ( @@ -108,11 +110,65 @@ export default function ComponentsPreview() { - {/* Radio Group Section */} -
-

- Radio Group Component -

+ {/* Checkbox Group Section */} +
+

+ Checkbox Group Component +

+ +
+
+
+

+ Standard Mode +

+
+ setCheckboxGroupValues(value)} + mode="standard" + options={[ + { value: "option1", label: "Checkbox label" }, + { + value: "option2", + label: "Checkbox label", + subtext: "Nunc sed hendrerit consequat.", + }, + ]} + /> +
+
+
+

+ Inverse Mode +

+
+ setCheckboxGroupValues(value)} + mode="inverse" + options={[ + { value: "option3", label: "Checkbox label" }, + { + value: "option4", + label: "Checkbox label", + subtext: "Nunc sed hendrerit consequat.", + }, + ]} + /> +
+
+
+
+
+ + {/* Radio Group Section */} +
+

+ Radio Group Component +

diff --git a/app/components/CheckboxGroup/CheckboxGroup.container.tsx b/app/components/CheckboxGroup/CheckboxGroup.container.tsx new file mode 100644 index 0000000..0afad08 --- /dev/null +++ b/app/components/CheckboxGroup/CheckboxGroup.container.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { memo, useCallback, useId, useState, useEffect } from "react"; +import { CheckboxGroupView } from "./CheckboxGroup.view"; +import type { CheckboxGroupProps } from "./CheckboxGroup.types"; + +const CheckboxGroupContainer = ({ + name, + value, + onChange, + mode = "standard", + disabled = false, + options = [], + className = "", + ...props +}: CheckboxGroupProps) => { + // Generate unique ID for accessibility if not provided + const generatedId = useId(); + const groupId = name || `checkbox-group-${generatedId}`; + + // Internal state to track checked values + const [checkedValues, setCheckedValues] = useState(value || []); + + // Sync internal state with external value prop + useEffect(() => { + if (value !== undefined) { + setCheckedValues(value); + } + }, [value]); + + const handleOptionChange = useCallback( + (optionValue: string, checked: boolean) => { + if (disabled) return; + + const newCheckedValues = checked + ? [...checkedValues, optionValue] + : checkedValues.filter((v) => v !== optionValue); + + setCheckedValues(newCheckedValues); + + if (onChange) { + onChange({ value: newCheckedValues }); + } + }, + [disabled, checkedValues, onChange], + ); + + return ( + + ); +}; + +CheckboxGroupContainer.displayName = "CheckboxGroup"; + +export default memo(CheckboxGroupContainer); diff --git a/app/components/CheckboxGroup/CheckboxGroup.types.ts b/app/components/CheckboxGroup/CheckboxGroup.types.ts new file mode 100644 index 0000000..0d98ee5 --- /dev/null +++ b/app/components/CheckboxGroup/CheckboxGroup.types.ts @@ -0,0 +1,28 @@ +export interface CheckboxOption { + value: string; + label: string; + subtext?: string; + ariaLabel?: string; +} + +export interface CheckboxGroupProps { + name?: string; + value?: string[]; + onChange?: (_data: { value: string[] }) => void; + mode?: "standard" | "inverse"; + disabled?: boolean; + options?: CheckboxOption[]; + className?: string; + "aria-label"?: string; +} + +export interface CheckboxGroupViewProps { + groupId: string; + value: string[]; + mode: "standard" | "inverse"; + disabled: boolean; + options: CheckboxOption[]; + className: string; + ariaLabel?: string; + onOptionChange: (_optionValue: string, _checked: boolean) => void; +} diff --git a/app/components/CheckboxGroup/CheckboxGroup.view.tsx b/app/components/CheckboxGroup/CheckboxGroup.view.tsx new file mode 100644 index 0000000..8c2d7e0 --- /dev/null +++ b/app/components/CheckboxGroup/CheckboxGroup.view.tsx @@ -0,0 +1,84 @@ +import Checkbox from "../Checkbox"; +import type { CheckboxGroupViewProps } from "./CheckboxGroup.types"; + +export function CheckboxGroupView({ + groupId, + value, + mode, + disabled, + options, + className, + ariaLabel, + onOptionChange, +}: CheckboxGroupViewProps) { + return ( +
+ {options.map((option) => { + const isChecked = value.includes(option.value); + + // If there's subtext, render checkbox without label and handle layout separately + if (option.subtext) { + return ( +
+ { + onOptionChange(option.value, checked); + }} + /> +
+ + {option.label} + + + {option.subtext} + +
+
+ ); + } + + // If no subtext, use Checkbox's built-in label + return ( + { + onOptionChange(option.value, checked); + }} + /> + ); + })} +
+ ); +} diff --git a/app/components/CheckboxGroup/index.tsx b/app/components/CheckboxGroup/index.tsx new file mode 100644 index 0000000..eabbbee --- /dev/null +++ b/app/components/CheckboxGroup/index.tsx @@ -0,0 +1 @@ +export { default } from "./CheckboxGroup.container"; diff --git a/stories/CheckboxGroup.stories.js b/stories/CheckboxGroup.stories.js new file mode 100644 index 0000000..11f5c54 --- /dev/null +++ b/stories/CheckboxGroup.stories.js @@ -0,0 +1,172 @@ +import React from "react"; +import CheckboxGroup from "../app/components/CheckboxGroup"; + +export default { + title: "Forms/CheckboxGroup", + component: CheckboxGroup, + parameters: { + layout: "centered", + backgrounds: { + default: "dark", + values: [ + { name: "light", value: "#ffffff" }, + { name: "dark", value: "#000000" }, + ], + }, + }, + argTypes: { + mode: { + control: "select", + options: ["standard", "inverse"], + description: "Visual mode of the checkbox group", + }, + disabled: { + control: "boolean", + description: "Whether the checkbox group is disabled", + }, + }, +}; + +export const Default = { + render: () => { + const [value, setValue] = React.useState([]); + + return ( + setValue(newValue)} + mode="standard" + options={[ + { value: "option1", label: "Checkbox label" }, + { value: "option2", label: "Checkbox label" }, + ]} + /> + ); + }, +}; + +export const WithSubtext = { + render: () => { + const [value, setValue] = React.useState([]); + + return ( + setValue(newValue)} + mode="standard" + options={[ + { value: "option1", label: "Checkbox label" }, + { + value: "option2", + label: "Checkbox label", + subtext: "Nunc sed hendrerit consequat.", + }, + ]} + /> + ); + }, +}; + +export const Inverse = { + render: () => { + const [value, setValue] = React.useState([]); + + return ( + setValue(newValue)} + mode="inverse" + options={[ + { value: "option1", label: "Checkbox label" }, + { value: "option2", label: "Checkbox label" }, + ]} + /> + ); + }, +}; + +export const InverseWithSubtext = { + render: () => { + const [value, setValue] = React.useState([]); + + return ( + setValue(newValue)} + mode="inverse" + options={[ + { value: "option1", label: "Checkbox label" }, + { + value: "option2", + label: "Checkbox label", + subtext: "Nunc sed hendrerit consequat.", + }, + ]} + /> + ); + }, +}; + +export const Disabled = { + render: () => ( + + ), +}; + +export const AllModes = () => { + const [standardValue, setStandardValue] = React.useState([]); + const [inverseValue, setInverseValue] = React.useState([]); + + return ( +
+
+

Standard Mode

+ setStandardValue(value)} + mode="standard" + options={[ + { value: "option1", label: "Checkbox label" }, + { + value: "option2", + label: "Checkbox label", + subtext: "Nunc sed hendrerit consequat.", + }, + ]} + /> +
+ +
+

Inverse Mode

+ setInverseValue(value)} + mode="inverse" + options={[ + { value: "option3", label: "Checkbox label" }, + { + value: "option4", + label: "Checkbox label", + subtext: "Nunc sed hendrerit consequat.", + }, + ]} + /> +
+
+ ); +};