Skip to content
31 changes: 31 additions & 0 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
},
"dependencies": {
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.1.15",
Expand Down
67 changes: 67 additions & 0 deletions client/src/app/test/checkbox/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"use client";
import { useState } from "react";

import { CheckboxGroup, CheckboxItem } from "@/components/checkbox-group";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";

const DATES = [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
];

export default function CheckboxTestPage() {
const [acceptTerms, setAcceptTerms] = useState<boolean>(false);
const [dates, setDates] = useState<string[]>([]);

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
alert(
`Submitted data:\nacceptTerms: ${acceptTerms.toString()}\ndates: ${JSON.stringify(dates)}`,
);
};

return (
<div className="min-h-screen p-8">
<div className="mx-auto my-5 flex w-full max-w-lg flex-col items-center justify-center">
<form onSubmit={handleSubmit} className="w-full rounded-md border p-4">
{/* a single checkbox */}
<p className="title mb-3">A single checkbox</p>
<Checkbox
checked={acceptTerms}
onCheckedChange={(checked) => setAcceptTerms(checked === true)} // Radix Output can be true | false | "indeterminate"
>
<p> Accept terms and conditions. </p>
</Checkbox>
{/* a checkbox group */}
<p className="title mb-3 mt-5">A checkbox group</p>
<CheckboxGroup
value={dates}
onValueChange={setDates}
className="grid-cols-2"
>
{DATES.map((date) => (
<CheckboxItem key={date} value={date} id={date}>
<p> {date} </p>
</CheckboxItem>
))}
</CheckboxGroup>
<Button type="submit" className="mx-auto mt-4">
Submit
</Button>
</form>
<p className="mt-4 text-center">
Current acceptTerms value: {acceptTerms.toString()}
</p>
<p className="mt-4 text-center">
Current dates message: {JSON.stringify(dates, null, 1)}
</p>
</div>
</div>
);
}
106 changes: 106 additions & 0 deletions client/src/components/checkbox-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* Usage:
*
* A group of checkbox: use `[value, setValue] = useState<string[]>(initialValue)` to get the selected value
* ```tsx
* <CheckboxGroup value={value} onValueChange={setValue}>
* <CheckboxItem value={value1}>{children}</CheckboxItem>
* <CheckboxItem value={value2}>{children}</CheckboxItem>
* </CheckboxGroup>
* ```
*/

"use client";
import type { CheckedState } from "@radix-ui/react-checkbox";
import React, { useCallback, useContext } from "react";

import { Checkbox } from "@/components/ui/checkbox";
import { cn } from "@/lib/utils";

type CheckboxGroupContextValue = {
value: string[];
onChange: (itemValue: string, checked: boolean) => void;
};

const CheckboxGroupContext =
React.createContext<CheckboxGroupContextValue | null>(null);

/**
* CheckboxGroup component for wrapping multiple CheckboxItem components and rendering a checkbox group.
*/
interface CheckboxGroupProps extends React.HTMLAttributes<HTMLDivElement> {
value: string[];
onValueChange: (value: string[]) => void;
}

const CheckboxGroup = React.forwardRef<HTMLDivElement, CheckboxGroupProps>(
({ className, value, onValueChange, children, ...props }, ref) => {
const onItemValueChange = useCallback(
(itemValue: string, checked: boolean) => {
if (checked) {
onValueChange([...value, itemValue]);
} else {
onValueChange(value.filter((v) => v !== itemValue));
}
},
[value, onValueChange],
);

return (
<CheckboxGroupContext.Provider
value={{ value, onChange: onItemValueChange }}
>
<div ref={ref} className={cn("grid gap-2", className)} {...props}>
{children}
</div>
</CheckboxGroupContext.Provider>
);
},
);

CheckboxGroup.displayName = "CheckboxGroup";

/**
* CheckboxItem component for rendering a single checkbox. Must used within CheckboxGroup.
*/
interface CheckboxItemProps
extends React.ComponentPropsWithoutRef<typeof Checkbox> {
value: string;
}

const CheckboxItem = React.forwardRef<
React.ElementRef<typeof Checkbox>,
CheckboxItemProps
>(
(
{ className, id: propsId, value, checked, onCheckedChange, ...props },
ref,
) => {
const context = useContext(CheckboxGroupContext);
const id = propsId || value;

if (!context) {
throw new Error("CheckboxItem must be used within a CheckboxGroup.");
}

const isChecked = context.value.includes(value);
const handleCheckedChange = (checked: CheckedState) =>
context.onChange(value, checked === true);

return (
<Checkbox
ref={ref}
id={id}
value={value}
className={className}
checked={isChecked}
onCheckedChange={handleCheckedChange}
{...props}
/>
);
},
);

CheckboxItem.displayName = "CheckboxItem";

export { CheckboxGroup, CheckboxItem };
58 changes: 58 additions & 0 deletions client/src/components/ui/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Usage:
*
* A single checkbox: use `[checked, setChecked] = useState<boolean>(initialValue)` to get the checkbox status
* ```tsx
* <Checkbox
* checked={checked}
* onCheckedChange={(checked) => setChecked(checked === true)} // As Radix Output can be true | false | "indeterminate", simple pass setChecked will cause Typescript complaint
* // optional props can be passed as in standard html checkbox
* >
* {children} // add label here
* </Checkbox>
* ```
*
* A group of checkbox: use CheckboxGroup
*
*/

"use client";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "use client" directive is missing at the top of this file. Other similar Radix UI component files in this codebase (e.g., accordion.tsx, select.tsx) include this directive, which is necessary for components that use React hooks like useCallback and useContext in Next.js App Router.

Copilot uses AI. Check for mistakes.
import React from "react";
import { FaCheck } from "react-icons/fa";

import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";

/**
* Checkbox component for rendering a single checkbox. Used standalone.
*/
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, children, ...props }, ref) => {
return (
<Label className="flex cursor-pointer items-center gap-1">
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"h-6 w-6 rounded-sm",
"border-2 border-gray-300",
"data-[state=checked]:border-none data-[state=checked]:bg-bloom-orbit data-[state=checked]:text-primary-foreground",
"disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator className="flex items-center justify-center">
<FaCheck className="h-4 w-4 text-white" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
{children}
</Label>
);
});

Checkbox.displayName = "Checkbox";

export { Checkbox };