Skip to content

Commit

Permalink
feat: 512 CheckboxGroup (#87)
Browse files Browse the repository at this point in the history
* initial CheckboxGroup

* feat: control variant of checkboxes

* fix: add label to checkboxItem

* fix: add flex wrap

* fix: add screen reader class

* fix: add export

* fix: 512 use value for id

* fix label for
  • Loading branch information
cade-exygy authored May 7, 2024
1 parent 00408dd commit 4729910
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 0 deletions.
1 change: 1 addition & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
11 changes: 11 additions & 0 deletions src/forms/CheckboxGroup.scss
Original file line number Diff line number Diff line change
@@ -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);
}
74 changes: 74 additions & 0 deletions src/forms/CheckboxGroup.tsx
Original file line number Diff line number Diff line change
@@ -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<ButtonProps, "size" | "variant"> {
/** 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<HTMLInputElement>) => {
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 (
<div id={props.id} className={classNames.join(" ")} data-testid={props.testId}>
{props.options.map((option) => (
<div key={`${props.id}-${option.value}-container`}>
<input
type="checkbox"
name={option.value}
id={`${props.id}-${option.value}`}
checked={isChecked(option)}
onChange={(e) => handleCheckboxChange(option.label, e)}
className="seeds-screen-reader-only"
/>
<label
className="seeds-button"
data-variant={
isChecked(option) ? props.checkedVariant || "primary" : props.variant || "secondary"
}
data-size={props.size || "lg"}
htmlFor={`${props.id}-${option.value}`}
>
{option.label}
</label>
</div>
))}
</div>
)
}

export default CheckboxGroup
34 changes: 34 additions & 0 deletions src/forms/__stories_/CheckboxGroup.docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ArgsTable } from "@storybook/addon-docs"
import CheckboxGroup from "../CheckboxGroup"

# &lt;CheckboxGroup /&gt;

## Properties

<ArgsTable of={CheckboxGroup} />

## 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` |
54 changes: 54 additions & 0 deletions src/forms/__stories_/CheckboxGroup.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<CheckboxItem[]>([])

return (
<CheckboxGroup
label={"My Checkbox Group"}
values={values}
options={options}
id={"MyCheckboxGroupNew"}
onChange={setValues}
/>
)
}

export const WithVariant = () => {
const options = [
{ label: "Option 1", value: "1" },
{ label: "Option 2", value: "2" },
{ label: "Option 3", value: "3" },
]
const [values, setValues] = useState<CheckboxItem[]>([])

return (
<CheckboxGroup
label={"My Checkbox Group"}
values={values}
options={options}
id={"MyCheckboxGroup"}
onChange={setValues}
variant="alert"
checkedVariant="success"
/>
)
}
42 changes: 42 additions & 0 deletions src/forms/__tests__/CheckboxGroup.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { render, cleanup, screen, fireEvent } from "@testing-library/react"
import CheckboxGroup, { CheckboxItem } from "forms/CheckboxGroup"

afterEach(cleanup)

describe("<CheckboxGroup>", () => {
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(
<CheckboxGroup
id="MyCheckboxGroup"
testId="MyCheckboxGroup"
className="test-class"
options={options}
values={values}
onChange={setValues}
/>
)
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"}])
})
})

0 comments on commit 4729910

Please sign in to comment.