diff --git a/index.ts b/index.ts index 23377c1..6ce1445 100644 --- a/index.ts +++ b/index.ts @@ -6,6 +6,7 @@ export { default as Alert } from "./src/blocks/Alert" export { default as Message } from "./src/blocks/Message" export { default as Toast } from "./src/blocks/Toast" export { default as Card } from "./src/blocks/Card" +export { default as CheckboxGroup } from "./src/forms/CheckboxGroup" export { default as FieldValue } from "./src/forms/FieldValue" export { default as FormErrorMessage } from "./src/forms/FormErrorMessage" export { default as Icon } from "./src/icons/Icon" diff --git a/src/forms/CheckboxGroup.scss b/src/forms/CheckboxGroup.scss new file mode 100644 index 0000000..1ae5af8 --- /dev/null +++ b/src/forms/CheckboxGroup.scss @@ -0,0 +1,11 @@ +.seeds-checkbox-group { + --inner-button-gap: var(--seeds-s3); + display: flex; + gap: var(--inner-button-gap); + flex-wrap: wrap; +} + +input[type="checkbox"]:focus-visible + .seeds-button { + outline: var(--seeds-focus-ring-outline); + box-shadow: var(--seeds-focus-ring-box-shadow); +} diff --git a/src/forms/CheckboxGroup.tsx b/src/forms/CheckboxGroup.tsx new file mode 100644 index 0000000..be65d4b --- /dev/null +++ b/src/forms/CheckboxGroup.tsx @@ -0,0 +1,74 @@ +import React from "react" + +import "./CheckboxGroup.scss" +import "../actions/Button.scss" +import { ButtonProps } from "actions/Button" + +export interface CheckboxItem { + label: string + value: string +} +export interface CheckboxGroupProps extends Pick { + /** Label content */ + label?: string + /** Current selected values*/ + values: CheckboxItem[] + /** An array of strings representing each item in the group*/ + options: CheckboxItem[] + /** Element ID */ + id: string + /** Additional CSS classes */ + className?: string + /** ID for selecting in tests */ + testId?: string + /** function to call when a checkbox is clicked*/ + onChange: (values: CheckboxItem[]) => void + /** Appearance of the checked input*/ + checkedVariant?: ButtonProps["variant"] +} + +const CheckboxGroup = (props: CheckboxGroupProps) => { + const classNames = ["seeds-checkbox-group"] + if (props.className) classNames.push(props.className) + + const handleCheckboxChange = (label: string, event: React.ChangeEvent) => { + const { name, checked } = event.target + const newValue = checked + ? [...props.values, { label: label, value: name }] + : props.values.filter((v) => v.value !== name) + props.onChange(newValue) + } + + const isChecked = (option: CheckboxItem) => { + return props.values.some((v) => v.value === option.value) + } + + return ( +
+ {props.options.map((option) => ( +
+ handleCheckboxChange(option.label, e)} + className="seeds-screen-reader-only" + /> + +
+ ))} +
+ ) +} + +export default CheckboxGroup diff --git a/src/forms/__stories_/CheckboxGroup.docs.mdx b/src/forms/__stories_/CheckboxGroup.docs.mdx new file mode 100644 index 0000000..7789ee1 --- /dev/null +++ b/src/forms/__stories_/CheckboxGroup.docs.mdx @@ -0,0 +1,34 @@ +import { ArgsTable } from "@storybook/addon-docs" +import CheckboxGroup from "../CheckboxGroup" + +# <CheckboxGroup /> + +## Properties + + + +## Theme Variables + +| Name | Description | Default | +| ------------------------------- | ----------------------------------------- | ----------------------- | +| `--inner-button-gap` | Space between elements | `--seeds-s3` | + +## Theme Variables from Button Styles + +| Name | Description | Default | +| ------------------------------- | ----------------------------------------- | ----------------------- | +| `--button-border-width` | Border width | `--seeds-border-2` | +| `--button-font-family` | Font family | `--seeds-font-alt-sans` | +| `--button-font-weight` | Font weight | `none` | +| `--button-interior-gap` | Space between icons/text | `--seeds-s3` | +| `--button-icon-size-percentage` | Relative size to base font | `75%` | +| `--button-icon-side-padding` | Space between an icon and the button edge | `--seeds-s4` | +| `--button-padding-sm` | Small button padding | | +| `--button-font-size-sm` | Small button font size | `--seeds-font-size-sm` | +| `--button-border-radius-sm` | Small button border radius | `--seeds-rounded` | +| `--button-padding-md` | Medium button padding | | +| `--button-font-size-md` | Medium button font size | `--seeds-font-size-md` | +| `--button-border-radius-md` | Medium button border radius | `--seeds-rounded` | +| `--button-padding-lg` | Large button padding | | +| `--button-font-size-lg` | Large button font size | `--seeds-font-size-lg` | +| `--button-border-radius-lg` | Large button border radius | `--seeds-rounded` | diff --git a/src/forms/__stories_/CheckboxGroup.stories.tsx b/src/forms/__stories_/CheckboxGroup.stories.tsx new file mode 100644 index 0000000..0305c74 --- /dev/null +++ b/src/forms/__stories_/CheckboxGroup.stories.tsx @@ -0,0 +1,54 @@ +import { useState } from "react" +import CheckboxGroup, { CheckboxItem } from "../CheckboxGroup" + +import MDXDocs from "./CheckboxGroup.docs.mdx" + +export default { + title: "Forms/CheckboxGroup", + component: CheckboxGroup, + parameters: { + docs: { + page: MDXDocs, + }, + }, +} + +export const Standalone = () => { + const options = [ + { label: "Option 1", value: "1" }, + { label: "Option 2", value: "2" }, + { label: "Option 3", value: "3" }, + ] + const [values, setValues] = useState([]) + + return ( + + ) +} + +export const WithVariant = () => { + const options = [ + { label: "Option 1", value: "1" }, + { label: "Option 2", value: "2" }, + { label: "Option 3", value: "3" }, + ] + const [values, setValues] = useState([]) + + return ( + + ) +} diff --git a/src/forms/__tests__/CheckboxGroup.test.tsx b/src/forms/__tests__/CheckboxGroup.test.tsx new file mode 100644 index 0000000..cb317eb --- /dev/null +++ b/src/forms/__tests__/CheckboxGroup.test.tsx @@ -0,0 +1,42 @@ +import { render, cleanup, screen, fireEvent } from "@testing-library/react" +import CheckboxGroup, { CheckboxItem } from "forms/CheckboxGroup" + +afterEach(cleanup) + +describe("", () => { + it("displays the CheckboxGroup", () => { + const options = [ + { label: "1", value: "1" }, + { label: "2", value: "2" }, + { label: "3", value: "3" }, + ] + let values: CheckboxItem[] = [] + const setValues = jest.fn((newValues) => { + values = newValues + }) + + render( + + ) + expect(screen.getByText(options[0].label)).toBeInTheDocument() + expect(screen.getByText(options[1].label)).toBeInTheDocument() + expect(screen.getByText(options[2].label)).toBeInTheDocument() + + expect(screen.getByTestId("MyCheckboxGroup")).not.toBeNull() + expect(screen.getByTestId("MyCheckboxGroup")).toHaveClass("test-class") + + const checkboxOne = screen.getByRole("checkbox", { name: /1/i }) + expect((checkboxOne as HTMLInputElement).checked).toBe(false) + + fireEvent.click(checkboxOne) + expect(setValues).toHaveBeenCalledWith([{label: "1", value: "1"}]) + expect(values).toEqual([{label: "1", value: "1"}]) + }) +})