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: Checkbox component #48

Merged
merged 1 commit into from
Nov 29, 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
120 changes: 120 additions & 0 deletions src/components/Checkbox/Checkbox.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Checkbox, CheckboxProps, CheckboxLabel } from "./index";
import { fn } from "@storybook/test";
import { useEffect, useMemo, useState } from "react";
const meta: Meta<CheckboxProps> = {
title: "Components/Checkbox",
component: Checkbox,
tags: ["autodocs"],
args: {
checked: false,
indeterminate: false,
onChange: fn(),
},
};

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

export const Interactive: Story = {
args: {},
render: (args) => {
const [checked, setChecked] = useState(args.checked);
useEffect(() => {
setChecked(args.checked);
}, [args.checked]);
return <Checkbox {...args} checked={checked} onChange={setChecked} />;
},
};

export const WithLabel: Story = {
args: {},
render: (args) => {
const [checked, setChecked] = useState(args.checked);
useEffect(() => {
setChecked(args.checked);
}, [args.checked]);
return (
<CheckboxLabel label="Checkbox">
<Checkbox {...args} checked={checked} onChange={setChecked} />
</CheckboxLabel>
);
},
};

export const WithLabelAndDescription: Story = {
args: {},
render: (args) => {
const [checked, setChecked] = useState(args.checked);
useEffect(() => {
setChecked(args.checked);
}, [args.checked]);
return (
<CheckboxLabel
label="Checkbox"
description="Some description of the checkbox"
>
<Checkbox {...args} checked={checked} onChange={setChecked} />
</CheckboxLabel>
);
},
};

export const NestingWithIndeterminateState: Story = {
args: {
indeterminate: false,
},
render: (args) => {
const [firstChildChecked, setFirstChildChecked] = useState(args.checked);
const [secondChildChecked, setSecondChildChecked] = useState(args.checked);

useEffect(() => {
setFirstChildChecked(true);
setSecondChildChecked(true);
}, [args.checked]);

const checked = useMemo(() => {
return firstChildChecked && secondChildChecked;
}, [firstChildChecked, secondChildChecked]);
const indeterminate = useMemo(() => {
return firstChildChecked || secondChildChecked;
}, [checked, firstChildChecked, secondChildChecked]);

return (
<div className="ink:flex ink:flex-col ink:gap-1">
<CheckboxLabel label="Top Level">
<Checkbox
{...args}
indeterminate={indeterminate}
checked={checked}
onChange={() => {
if (checked || indeterminate) {
setFirstChildChecked(false);
setSecondChildChecked(false);
} else {
setFirstChildChecked(true);
setSecondChildChecked(true);
}
}}
/>
</CheckboxLabel>
<div className="ink:flex ink:flex-col ink:pl-2 ink:gap-1">
<CheckboxLabel label="First Child">
<Checkbox
{...args}
checked={firstChildChecked || checked}
onChange={setFirstChildChecked}
/>
</CheckboxLabel>
<CheckboxLabel label="Second Child">
<Checkbox
{...args}
checked={secondChildChecked || checked}
onChange={setSecondChildChecked}
/>
</CheckboxLabel>
</div>
</div>
);
},
};
66 changes: 66 additions & 0 deletions src/components/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Checkbox as HeadlessCheckbox } from "@headlessui/react";
import { classNames } from "../../util/classes";

export interface CheckboxProps {
checked: boolean;
indeterminate?: boolean;
onChange: (enabled: boolean) => void;
}

export const Checkbox: React.FC<CheckboxProps> = ({
checked,
indeterminate,
onChange,
}) => {
return (
<HeadlessCheckbox
checked={checked}
onChange={onChange}
indeterminate={!checked && indeterminate}
className={classNames(
"ink:group ink:relative ink:flex ink:items-center ink:justify-center ink:size-3 ink:shrink-0 ink:cursor-pointer ink:rounded-xs ink:box-border",
"ink:transition-colors ink:duration-200 ink:ease-in-out",
"ink:border-2 ink:border-transparent ink:bg-background-container ink:shadow-xs",
"ink:ring-text-on-secondary ink:focus-visible:outline-none ink:focus-visible:text-on-primary ink:focus-visible:ring-2 ink:focus-visible:ring-offset-2",
"ink:data-checked:bg-button-primary ink:data-checked:hover:bg-button-primary-hover",
"ink:data-indeterminate:bg-button-primary ink:data-indeterminate:hover:bg-button-primary-hover",
"ink:flex ink:items-center"
)}
>
<div className="ink:absolute ink:inset-0 ink:flex ink:items-center ink:justify-center">
{/** See if those SVGs should be icons in our standard icon library. */}
<svg
className="ink:size-1.5 ink:text-text-on-primary ink:group-data-indeterminate:opacity-0 ink:transition-opacity ink:duration-200 ink:ease-in-out opacity-0 starting:opacity-100"
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M11.5905 0.748484C12.0362 1.07459 12.1332 1.70028 11.8071 2.14601L5.30384 11.0349C5.14317 11.2545 4.89996 11.3992 4.6303 11.4355C4.36064 11.4718 4.08781 11.3967 3.87476 11.2274L0.378025 8.44967C-0.0544173 8.10614 -0.126496 7.47709 0.217033 7.04465C0.560562 6.6122 1.18961 6.54013 1.62205 6.88365L4.30408 9.01423L10.193 0.965086C10.5191 0.519357 11.1448 0.422381 11.5905 0.748484Z"
fill="currentColor"
/>
</svg>
</div>

<svg
className="ink:size-1.5 ink:text-text-on-primary ink:group-not-data-indeterminate:opacity-0 ink:transition-opacity ink:duration-200 ink:ease-in-out opacity-0 starting:opacity-100"
width="12"
height="2"
viewBox="0 0 12 2"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0 1C0 0.447715 0.447715 0 1 0H11C11.5523 0 12 0.447715 12 1C12 1.55228 11.5523 2 11 2H1C0.447715 2 0 1.55228 0 1Z"
fill="currentColor"
/>
</svg>
</HeadlessCheckbox>
);
};
30 changes: 30 additions & 0 deletions src/components/Checkbox/CheckboxLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Description, Field, Label } from "@headlessui/react";
import { PropsWithChildren } from "react";

export interface CheckboxLabelProps extends PropsWithChildren {
label: React.ReactNode;
description?: React.ReactNode;
}

export const CheckboxLabel: React.FC<CheckboxLabelProps> = ({
label,
description,
children,
}) => {
return (
<Field className="ink:flex ink:flex-col ink:font-default">
<div className="ink:flex ink:items-center ink:gap-1">
{children}
<Label className="ink:cursor-pointer ink:h-3 ink:flex ink:items-center ink:justify-center ink:text-body-2 ink:text-text-default">
{label}
</Label>
</div>

{description && (
<Description className="ink:text-caption-2 ink:text-text-default">
{description}
</Description>
)}
</Field>
);
};
2 changes: 2 additions & 0 deletions src/components/Checkbox/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./Checkbox";
export * from "./CheckboxLabel";
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./Button";
export * from "./Checkbox";
export * from "./Input";
export * from "./Modal";
export * from "./Popover";
Expand Down
1 change: 1 addition & 0 deletions src/tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@

/* Shadows */
--shadow-*: initial;
--shadow-xs: 0px 1px 2px 0px rgba(16, 24, 40, 0.04);
--shadow-menu: 0px 8px 24px -8px #160f1f1a;
--shadow-modal: 0px 16px 64px -32px #160f1f1a;
--shadow-layout: 0px 16px 64px -32px #160f1f0d;
Expand Down
Loading