-
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: RadioButtonGroup component (#1517)
## ✅ 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~ - [X] Specs are up-to-date - [X] Console is free from warnings - [X] No accessibility violations are reported - [X] Cross-browser check is performed (Chrome, Safari, Firefox) - [X] Changeset is added
- Loading branch information
Showing
8 changed files
with
485 additions
and
2 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 | ||
--- | ||
|
||
Add RadioButtonGroup 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
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,56 @@ | ||
import React from "react"; | ||
import { ArgTypes, Canvas, Meta, Controls } from "@storybook/blocks"; | ||
import { RadioButtonGroup } from "./RadioButtonGroup"; | ||
import * as RadioButtonGroupStories from "./RadioButtonGroup.stories"; | ||
|
||
<Meta of={RadioButtonGroupStories} /> | ||
|
||
# RadioButtonGroup | ||
|
||
A `<RadioButtonGroup />` is a group of connected buttons that act as a radio group. | ||
|
||
<Canvas of={RadioButtonGroupStories.Default} /> | ||
|
||
## Selection Mode | ||
|
||
A `<RadioButtonGroup />` can be used in `single` or `multiple` selection mode. | ||
|
||
<Canvas of={RadioButtonGroupStories.SelectionMode} /> | ||
<Controls | ||
of={RadioButtonGroupStories.SelectionMode} | ||
include={["selectionMode"]} | ||
/> | ||
|
||
## Disabled | ||
|
||
A `<RadioButtonGroup />` can be disabled. | ||
|
||
<Canvas of={RadioButtonGroupStories.EnabledDisabled} /> | ||
<Controls | ||
of={RadioButtonGroupStories.EnabledDisabled} | ||
include={["isDisabled"]} | ||
/> | ||
|
||
Individual `<RadioButtonGroup.Button />` elements can be disabled. | ||
|
||
<Canvas of={RadioButtonGroupStories.EnabledDisabledButton} /> | ||
|
||
## Disallow Empty Selection | ||
|
||
A `<RadioButtonGroup />` can disallow empty selection. | ||
|
||
<Canvas of={RadioButtonGroupStories.DisallowEmptySelection} /> | ||
<Controls | ||
of={RadioButtonGroupStories.DisallowEmptySelection} | ||
include={["disallowEmptySelection"]} | ||
/> | ||
|
||
## Properties | ||
|
||
### RadioButtonGroup | ||
|
||
<ArgTypes of={RadioButtonGroup} /> | ||
|
||
### RadioButtonGroup.Button | ||
|
||
<ArgTypes of={RadioButtonGroup.Button} /> |
48 changes: 48 additions & 0 deletions
48
easy-ui-react/src/RadioButtonGroup/RadioButtonGroup.module.scss
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,48 @@ | ||
@use "../styles/common" as *; | ||
@use "../Button/mixins" as Button; | ||
@use "../styles/unstyled"; | ||
|
||
.RadioButtonGroupButton { | ||
@include unstyled.button; | ||
min-width: 32px; | ||
padding: calc(#{design-token("space.0.5")} - 1px) // subracting 1 for border | ||
design-token("space.1"); | ||
border: design-token("shape.border_width.1") solid | ||
design-token("color.neutral.300"); | ||
outline: none; | ||
color: design-token("color.primary.800"); | ||
margin-inline-start: calc(#{design-token("shape.border_width.1")} * -1); | ||
position: relative; | ||
display: inline-flex; | ||
justify-content: center; | ||
align-items: center; | ||
|
||
&:first-child { | ||
border-radius: design-token("shape.border_radius.md") 0 0 | ||
design-token("shape.border_radius.md"); | ||
margin-inline-start: 0; | ||
} | ||
|
||
&:last-child { | ||
border-radius: 0 design-token("shape.border_radius.md") | ||
design-token("shape.border_radius.md") 0; | ||
} | ||
|
||
&:disabled { | ||
color: design-token("color.neutral.300"); | ||
cursor: not-allowed; | ||
} | ||
|
||
&[data-selected] { | ||
background-color: component-token("radio-button-group", "color"); | ||
border-color: component-token("radio-button-group", "color"); | ||
color: design-token("color.neutral.000"); | ||
z-index: 2; | ||
} | ||
|
||
&[data-selected]:disabled { | ||
background-color: design-token("color.neutral.100"); | ||
color: design-token("color.neutral.600"); | ||
border-color: design-token("color.neutral.300"); | ||
} | ||
} |
105 changes: 105 additions & 0 deletions
105
easy-ui-react/src/RadioButtonGroup/RadioButtonGroup.stories.tsx
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,105 @@ | ||
import { Meta, StoryObj } from "@storybook/react"; | ||
import React from "react"; | ||
import { createColorTokensControl } from "../utilities/storybook"; | ||
import { RadioButtonGroup, RadioButtonGroupProps } from "./RadioButtonGroup"; | ||
|
||
type Story = StoryObj<typeof RadioButtonGroup>; | ||
|
||
const meta: Meta<typeof RadioButtonGroup> = { | ||
title: "Components/RadioButtonGroup", | ||
component: RadioButtonGroup, | ||
args: { | ||
color: "primary.500", | ||
}, | ||
argTypes: { | ||
color: { | ||
...createColorTokensControl(), | ||
table: { | ||
type: { summary: null }, | ||
}, | ||
}, | ||
}, | ||
}; | ||
|
||
const Template = (args: RadioButtonGroupProps) => { | ||
const { children, ...restArgs } = args; | ||
return <RadioButtonGroup {...restArgs}>{children}</RadioButtonGroup>; | ||
}; | ||
|
||
export default meta; | ||
|
||
export const Default: Story = { | ||
render: Template.bind({}), | ||
args: { | ||
children: ( | ||
<> | ||
<RadioButtonGroup.Button id="in">in</RadioButtonGroup.Button> | ||
<RadioButtonGroup.Button id="cm">cm</RadioButtonGroup.Button> | ||
</> | ||
), | ||
defaultSelectedKeys: ["in"], | ||
}, | ||
}; | ||
|
||
export const SelectionMode: Story = { | ||
render: Template.bind({}), | ||
args: { | ||
children: ( | ||
<> | ||
<RadioButtonGroup.Button id="bold">Bold</RadioButtonGroup.Button> | ||
<RadioButtonGroup.Button id="italic">Italic</RadioButtonGroup.Button> | ||
<RadioButtonGroup.Button id="underline"> | ||
Underline | ||
</RadioButtonGroup.Button> | ||
</> | ||
), | ||
selectionMode: "multiple", | ||
}, | ||
}; | ||
|
||
export const EnabledDisabled: Story = { | ||
render: Template.bind({}), | ||
args: { | ||
children: ( | ||
<> | ||
<RadioButtonGroup.Button id="in">in</RadioButtonGroup.Button> | ||
<RadioButtonGroup.Button id="cm">cm</RadioButtonGroup.Button> | ||
</> | ||
), | ||
isDisabled: true, | ||
defaultSelectedKeys: ["in"], | ||
}, | ||
}; | ||
|
||
export const EnabledDisabledButton: Story = { | ||
render: Template.bind({}), | ||
args: { | ||
children: ( | ||
<> | ||
<RadioButtonGroup.Button id="in">in</RadioButtonGroup.Button> | ||
<RadioButtonGroup.Button isDisabled id="cm"> | ||
cm | ||
</RadioButtonGroup.Button> | ||
<RadioButtonGroup.Button id="ft">ft</RadioButtonGroup.Button> | ||
<RadioButtonGroup.Button isDisabled id="mi"> | ||
mi | ||
</RadioButtonGroup.Button> | ||
</> | ||
), | ||
defaultSelectedKeys: ["in"], | ||
}, | ||
}; | ||
|
||
export const DisallowEmptySelection: Story = { | ||
render: Template.bind({}), | ||
args: { | ||
children: ( | ||
<> | ||
<RadioButtonGroup.Button id="in">in</RadioButtonGroup.Button> | ||
<RadioButtonGroup.Button id="cm">cm</RadioButtonGroup.Button> | ||
</> | ||
), | ||
disallowEmptySelection: true, | ||
defaultSelectedKeys: ["in"], | ||
}, | ||
}; |
113 changes: 113 additions & 0 deletions
113
easy-ui-react/src/RadioButtonGroup/RadioButtonGroup.test.tsx
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,113 @@ | ||
import { screen } from "@testing-library/react"; | ||
import React from "react"; | ||
import { vi } from "vitest"; | ||
import { render, selectCheckbox } from "../utilities/test"; | ||
import { RadioButtonGroup } from "./RadioButtonGroup"; | ||
|
||
describe("<RadioButtonGroup />", () => { | ||
beforeEach(() => { | ||
vi.useFakeTimers(); | ||
}); | ||
|
||
afterEach(() => { | ||
vi.useRealTimers(); | ||
}); | ||
|
||
it("should render a radio group", () => { | ||
render( | ||
<RadioButtonGroup> | ||
<RadioButtonGroup.Button id="first"> | ||
First Button | ||
</RadioButtonGroup.Button> | ||
<RadioButtonGroup.Button id="second"> | ||
Second Button | ||
</RadioButtonGroup.Button> | ||
</RadioButtonGroup>, | ||
); | ||
expect(screen.getByRole("radiogroup")).toBeInTheDocument(); | ||
expect(screen.getAllByRole("radio").length).toBe(2); | ||
expect(screen.getAllByRole("radio")[0]).toHaveAttribute("aria-checked"); | ||
}); | ||
|
||
it("should update when a button is clicked", async () => { | ||
const { user } = render( | ||
<RadioButtonGroup defaultSelectedKeys={["first"]}> | ||
<RadioButtonGroup.Button id="first">First item</RadioButtonGroup.Button> | ||
<RadioButtonGroup.Button id="second"> | ||
Second item | ||
</RadioButtonGroup.Button> | ||
</RadioButtonGroup>, | ||
); | ||
const radios = screen.getAllByRole("radio"); | ||
expect(radios[0]).toBeChecked(); | ||
|
||
await selectCheckbox(user, radios[1]); | ||
expect(radios[0]).not.toBeChecked(); | ||
expect(radios[1]).toBeChecked(); | ||
}); | ||
|
||
it("should support disabling the group", async () => { | ||
const { user } = render( | ||
<RadioButtonGroup defaultSelectedKeys={["first"]} isDisabled> | ||
<RadioButtonGroup.Button id="first">First item</RadioButtonGroup.Button> | ||
<RadioButtonGroup.Button id="second"> | ||
Second item | ||
</RadioButtonGroup.Button> | ||
</RadioButtonGroup>, | ||
); | ||
const radios = screen.getAllByRole("radio"); | ||
expect(radios[0]).toBeChecked(); | ||
|
||
await selectCheckbox(user, radios[1]); | ||
expect(radios[0]).toBeChecked(); | ||
expect(radios[1]).not.toBeChecked(); | ||
}); | ||
|
||
it("should support disabling individual buttons", async () => { | ||
const { user } = render( | ||
<RadioButtonGroup defaultSelectedKeys={["first"]} isDisabled> | ||
<RadioButtonGroup.Button id="first">First item</RadioButtonGroup.Button> | ||
<RadioButtonGroup.Button id="second" isDisabled> | ||
Second item | ||
</RadioButtonGroup.Button> | ||
</RadioButtonGroup>, | ||
); | ||
const radios = screen.getAllByRole("radio"); | ||
expect(radios[0]).toBeChecked(); | ||
|
||
await selectCheckbox(user, radios[1]); | ||
expect(radios[0]).toBeChecked(); | ||
expect(radios[1]).not.toBeChecked(); | ||
}); | ||
|
||
it("should support controlled use", async () => { | ||
const handleChange = vi.fn(); | ||
const { user, rerender } = render( | ||
<RadioButtonGroup | ||
selectedKeys={["second"]} | ||
onSelectionChange={handleChange} | ||
> | ||
<RadioButtonGroup.Button id="first">First item</RadioButtonGroup.Button> | ||
<RadioButtonGroup.Button id="second"> | ||
Second item | ||
</RadioButtonGroup.Button> | ||
</RadioButtonGroup>, | ||
); | ||
|
||
const radios = screen.getAllByRole("radio"); | ||
await selectCheckbox(user, radios[0]); | ||
expect(handleChange).toBeCalled(); | ||
expect(screen.getAllByRole("radio")[0]).not.toBeChecked(); | ||
|
||
rerender( | ||
<RadioButtonGroup selectedKeys={["second"]}> | ||
<RadioButtonGroup.Button id="first">First item</RadioButtonGroup.Button> | ||
<RadioButtonGroup.Button id="second"> | ||
Second item | ||
</RadioButtonGroup.Button> | ||
</RadioButtonGroup>, | ||
); | ||
|
||
expect(screen.getAllByRole("radio")[1]).toBeChecked(); | ||
}); | ||
}); |
Oops, something went wrong.