-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: CheckableCard component (#1443)
## 📝 Changes Please provide a brief summary of the changes made and why they were made. Include any notes, screenshots, or videos that may be helpful for developers reviewing this pull request. ## ✅ Checklist Easy UI has certain UX standards that must be met. In general, non-trivial changes should meet the following criteria: - [x] Visuals match Design Specs in Figma - [x] Stories accompany any component changes - [x] Code is in accordance with our style guide - [x] Design tokens are utilized - [x] Unit tests accompany any component changes - [x] TSDoc is written for any API surface area - [ ] Specs are up-to-date - [x] Console is free from warnings - [ ] No accessibility violations are reported - [ ] Cross-browser check is performed (Chrome, Safari, Firefox) - [x] Changeset is added ~Strikethrough~ any items that are not applicable to this pull request. --------- Co-authored-by: Stephen Watkins <[email protected]>
- Loading branch information
1 parent
380d1ad
commit 4314cde
Showing
15 changed files
with
421 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@easypost/easy-ui": minor | ||
--- | ||
|
||
feat: CheckableCard component |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
# `CheckableCard` Component Specification | ||
|
||
## Overview | ||
|
||
A `CheckableCard` is a styled container with a `Checkbox` form element. | ||
|
||
### Use Cases | ||
|
||
- Use a CheckableCard to display a checkbox for users inside a styled container | ||
|
||
### Features | ||
|
||
- Supports use of `Checkbox` form element within a `Card` container | ||
- Supports existing features of `Card` component | ||
- Supports existing features of `Checkbox` component | ||
|
||
### Prior Art | ||
|
||
### Design | ||
|
||
Design of `CheckableCard` is comprised of the `Checkbox` component wrapped by the `Card` container component. | ||
|
||
### API | ||
|
||
```ts | ||
export type CheckableCardProps = CheckboxProps & { | ||
/** | ||
* Card children | ||
*/ | ||
children: ReactNode; | ||
}; | ||
``` | ||
|
||
### Example Usage | ||
|
||
```tsx | ||
import { CheckableCard } from "@easypost/easy-ui/CheckableCard"; | ||
|
||
function Component() { | ||
return <CheckableCard>Checkbox item</CheckableCard>; | ||
} | ||
``` | ||
|
||
_Default value:_ | ||
|
||
```tsx | ||
import { CheckableCard } from "@easypost/easy-ui/CheckableCard"; | ||
|
||
function Component() { | ||
return <CheckableCard defaultSelected={true}>Checkbox item</CheckableCard>; | ||
} | ||
``` | ||
|
||
_Controlled:_ | ||
|
||
```tsx | ||
import { CheckableCard } from "@easypost/easy-ui/CheckableCard"; | ||
|
||
function Component() { | ||
const [isSelected, setIsSelected] = useState(false); | ||
return ( | ||
<CheckableCard | ||
isSelected={isSelected} | ||
onChange={(isSelected) => setIsSelected(isSelected)} | ||
> | ||
Checkbox item | ||
</CheckableCard> | ||
); | ||
} | ||
``` | ||
|
||
_Disabled:_ | ||
|
||
```tsx | ||
import { CheckableCard } from "@easypost/easy-ui/CheckableCard"; | ||
|
||
function Component() { | ||
return <CheckableCard isDisabled={true}>Checkbox item</CheckableCard>; | ||
} | ||
``` | ||
|
||
_Read-only:_ | ||
|
||
```tsx | ||
import { CheckableCard } from "@easypost/easy-ui/CheckableCard"; | ||
|
||
function Component() { | ||
return ( | ||
<CheckableCard isSelected={true} isReadOnly={true}> | ||
Checkbox item | ||
</CheckableCard> | ||
); | ||
} | ||
``` | ||
|
||
_Form submission data:_ | ||
|
||
```tsx | ||
import { CheckableCard } from "@easypost/easy-ui/CheckableCard"; | ||
|
||
function Component() { | ||
return ( | ||
<CheckableCard name="checkable-card-name" value="checkable-card-value"> | ||
Checkbox item | ||
</CheckableCard> | ||
); | ||
} | ||
``` | ||
|
||
_Error:_ | ||
|
||
```tsx | ||
import { CheckableCard } from "@easypost/easy-ui/CheckableCard"; | ||
|
||
function Component() { | ||
return ( | ||
<CheckableCard | ||
validationState="invalid" | ||
errorText="This is required to proceed" | ||
> | ||
Checkbox item | ||
</CheckableCard> | ||
); | ||
} | ||
``` | ||
|
||
### Anatomy | ||
|
||
The `CheckableCard` component will render a `Card` component that wraps a `Checkbox` component. | ||
|
||
```tsx | ||
function CheckableCard() { | ||
return ( | ||
<Card> | ||
<Checkbox | ||
isSelected={isSelected} | ||
onChange={(isSelected) => setIsSelected(isSelected)} | ||
> | ||
Checkbox item | ||
</Checkbox> | ||
</Card> | ||
); | ||
} | ||
``` | ||
|
||
--- | ||
|
||
## Behavior | ||
|
||
### Accessibility | ||
|
||
### Dependencies | ||
|
||
- React Aria's `useCheckbox`, `useFocusRing`, `VisuallyHidden`, `useToggleState`, and `ValidationState` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import React from "react"; | ||
import { ArgTypes, Canvas, Meta, Controls } from "@storybook/blocks"; | ||
import { CheckableCard } from "./CheckableCard"; | ||
import * as CheckableCardStories from "./CheckableCard.stories"; | ||
|
||
<Meta of={CheckableCardStories} /> | ||
|
||
# CheckableCard | ||
|
||
<Canvas of={CheckableCardStories.Default} /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
@use "../styles/common" as *; | ||
|
||
.textContainer { | ||
padding-left: 36px; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { Meta, StoryObj } from "@storybook/react"; | ||
import React from "react"; | ||
import { CheckableCardProps, CheckableCard } from "./CheckableCard"; | ||
|
||
type Story = StoryObj<typeof CheckableCard>; | ||
|
||
const Template = (args: CheckableCardProps) => <CheckableCard {...args} />; | ||
|
||
const meta: Meta<typeof CheckableCard> = { | ||
title: "Components/Cards/CheckableCard", | ||
component: CheckableCard, | ||
}; | ||
|
||
export default meta; | ||
|
||
export const Default: Story = { | ||
render: Template.bind({}), | ||
args: { | ||
header: "Header", | ||
children: "Content", | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { screen } from "@testing-library/react"; | ||
import React from "react"; | ||
import { vi } from "vitest"; | ||
import { hoverOverTooltipTrigger } from "../Tooltip/Tooltip.test"; | ||
import { render, selectCheckbox } from "../utilities/test"; | ||
import { CheckableCard } from "./CheckableCard"; | ||
|
||
describe("<CheckableCard />", () => { | ||
beforeEach(() => { | ||
vi.useFakeTimers(); | ||
}); | ||
|
||
afterEach(() => { | ||
vi.useRealTimers(); | ||
}); | ||
|
||
it("should render a CheckableCard", () => { | ||
render(<CheckableCard header="Header">Content</CheckableCard>); | ||
expect(screen.getByRole("checkbox")).toBeInTheDocument(); | ||
expect(screen.getByRole("checkbox")).not.toBeChecked(); | ||
}); | ||
|
||
it("should support uncontrolled", async () => { | ||
const { user } = render( | ||
<CheckableCard header="Header" defaultSelected={true}> | ||
Checkbox item | ||
</CheckableCard>, | ||
); | ||
expect(screen.getByRole("checkbox")).toBeChecked(); | ||
await selectCheckbox(user, screen.getByRole("checkbox")); | ||
expect(screen.getByRole("checkbox")).not.toBeChecked(); | ||
}); | ||
|
||
it("should support controlled", async () => { | ||
const handleChange = vi.fn(); | ||
const { user, rerender } = render( | ||
<CheckableCard header="Header" isSelected={false} onChange={handleChange}> | ||
Checkbox item | ||
</CheckableCard>, | ||
); | ||
await selectCheckbox(user, screen.getByRole("checkbox")); | ||
expect(handleChange).toBeCalled(); | ||
expect(screen.getByRole("checkbox")).not.toBeChecked(); | ||
rerender( | ||
<CheckableCard header="Header" isSelected={true} onChange={handleChange}> | ||
Checkbox item | ||
</CheckableCard>, | ||
); | ||
expect(screen.getByRole("checkbox")).toBeChecked(); | ||
}); | ||
|
||
it("should support isDisabled", () => { | ||
render( | ||
<CheckableCard header="Header" isDisabled> | ||
Checkbox item | ||
</CheckableCard>, | ||
); | ||
expect(screen.getByRole("checkbox")).toBeDisabled(); | ||
}); | ||
|
||
it("should support isReadOnly", async () => { | ||
const { user } = render( | ||
<CheckableCard header="Header" isReadOnly> | ||
Checkbox item | ||
</CheckableCard>, | ||
); | ||
await selectCheckbox(user, screen.getByRole("checkbox")); | ||
expect(screen.getByRole("checkbox")).not.toBeChecked(); | ||
}); | ||
|
||
it("should support isIndeterminate", () => { | ||
render( | ||
<CheckableCard header="Header" isIndeterminate> | ||
Checkbox item | ||
</CheckableCard>, | ||
); | ||
expect( | ||
(screen.getByRole("checkbox") as HTMLInputElement).indeterminate, | ||
).toBeTruthy(); | ||
}); | ||
|
||
it("should support errors", async () => { | ||
const { user } = render( | ||
<CheckableCard | ||
header="Header" | ||
validationState="invalid" | ||
errorText="This field is required" | ||
> | ||
Checkbox item | ||
</CheckableCard>, | ||
); | ||
await hoverOverTooltipTrigger(user, screen.getByText("Error")); | ||
expect(screen.getByRole("checkbox")).toBeInvalid(); | ||
expect(screen.getByText("This field is required")).toBeInTheDocument(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import React, { ReactNode } from "react"; | ||
import { Card } from "../Card"; | ||
import { Checkbox, CheckboxProps } from "../Checkbox"; | ||
import { HorizontalStack } from "../HorizontalStack"; | ||
import { Text } from "../Text"; | ||
import { VerticalStack } from "../VerticalStack"; | ||
|
||
import styles from "./CheckableCard.module.scss"; | ||
|
||
export type CheckableCardProps = Omit<CheckboxProps, "size"> & { | ||
/** | ||
* The header for the checkable card. | ||
*/ | ||
header: ReactNode; | ||
|
||
/** | ||
* The content for the checkable card. | ||
*/ | ||
children: ReactNode; | ||
}; | ||
|
||
/** | ||
* A styled container with a `Checkbox` form element. | ||
* | ||
* @example | ||
* ```tsx | ||
* <CheckableCard | ||
* isSelected={isSelected} | ||
* onChange={(isSelected) => setIsSelected(isSelected)} | ||
* header="Header" | ||
* > | ||
* Content | ||
* </CheckableCard> | ||
* ``` | ||
*/ | ||
export function CheckableCard(props: CheckableCardProps) { | ||
const { header, children, ...checkboxProps } = props; | ||
return ( | ||
<Card> | ||
<VerticalStack gap="2"> | ||
<HorizontalStack gap="1.5" blockAlign="center"> | ||
<VerticalStack gap="1"> | ||
<Checkbox {...checkboxProps} size="lg"> | ||
<Text weight="semibold" as="h3"> | ||
{header} | ||
</Text> | ||
</Checkbox> | ||
<div className={styles.textContainer}> | ||
<Text weight="medium" color="neutral.500"> | ||
{children} | ||
</Text> | ||
</div> | ||
</VerticalStack> | ||
</HorizontalStack> | ||
</VerticalStack> | ||
</Card> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./CheckableCard"; |
Oops, something went wrong.