-
Notifications
You must be signed in to change notification settings - Fork 68
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: RadioCardGroup component (#34)
* first draft radio cards * update radio card * add tests * update licencse
- Loading branch information
1 parent
9d2ba1e
commit e6acc50
Showing
5 changed files
with
383 additions
and
0 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
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,98 @@ | ||
// Tremor Raw Radio Card [v0.0.0] | ||
|
||
import React from "react" | ||
import * as RadioGroupPrimitives from "@radix-ui/react-radio-group" | ||
|
||
import { cx } from "../../utils/cx" | ||
import { focusInput } from "../../utils/focusInput" | ||
import { focusRing } from "../../utils/focusRing" | ||
|
||
const RadioCardGroup = React.forwardRef< | ||
React.ElementRef<typeof RadioGroupPrimitives.Root>, | ||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitives.Root> | ||
>(({ className, ...props }, forwardedRef) => { | ||
return ( | ||
<RadioGroupPrimitives.Root | ||
ref={forwardedRef} | ||
className={cx("grid gap-2", className)} | ||
{...props} | ||
/> | ||
) | ||
}) | ||
RadioCardGroup.displayName = "RadioCardGroup" | ||
|
||
const RadioCardItem = React.forwardRef< | ||
React.ElementRef<typeof RadioGroupPrimitives.Item>, | ||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitives.Item> | ||
>(({ className, children, ...props }, forwardedRef) => { | ||
return ( | ||
<RadioGroupPrimitives.Item | ||
ref={forwardedRef} | ||
className={cx( | ||
// base | ||
"group relative w-full rounded-md border p-4 text-left shadow-sm transition focus:outline-none", | ||
// background color | ||
"bg-white dark:bg-gray-950", | ||
// border color | ||
"border-gray-200 dark:border-gray-800", | ||
"data-[state=checked]:border-blue-500", | ||
"data-[state=checked]:dark:border-blue-500", | ||
// disabled | ||
"data-[disabled]:border-gray-100 data-[disabled]:dark:border-gray-800", | ||
"data-[disabled]:bg-gray-50 data-[disabled]:shadow-none data-[disabled]:dark:bg-gray-900", | ||
focusInput, | ||
className, | ||
)} | ||
{...props} | ||
> | ||
{children} | ||
</RadioGroupPrimitives.Item> | ||
) | ||
}) | ||
RadioCardItem.displayName = "RadioCardItem" | ||
|
||
const RadioCardIndicator = React.forwardRef< | ||
React.ElementRef<typeof RadioGroupPrimitives.Indicator>, | ||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitives.Indicator> | ||
>(({ className, ...props }, forwardedRef) => { | ||
return ( | ||
<div | ||
className={cx( | ||
// base | ||
"relative flex size-4 shrink-0 appearance-none items-center justify-center rounded-full border shadow-sm outline-none", | ||
// border color | ||
"border-gray-300 dark:border-gray-800", | ||
// background color | ||
"bg-white dark:bg-gray-950", | ||
// checked | ||
"group-data-[state=checked]:border-0 group-data-[state=checked]:border-transparent group-data-[state=checked]:bg-blue-500", | ||
// disabled | ||
"group-data-[disabled]:border-gray-300 group-data-[disabled]:bg-gray-100 group-data-[disabled]:text-gray-400", | ||
"group-data-[disabled]:dark:border-gray-700 group-data-[disabled]:dark:bg-gray-800", | ||
// focus | ||
focusRing, | ||
className, | ||
)} | ||
> | ||
<RadioGroupPrimitives.Indicator | ||
ref={forwardedRef} | ||
className={cx("flex items-center justify-center")} | ||
{...props} | ||
> | ||
<div | ||
className={cx( | ||
// base | ||
"size size-1.5 shrink-0 rounded-full", | ||
// indicator | ||
"bg-white", | ||
// disabled | ||
"group-data-[disabled]:bg-gray-400 group-data-[disabled]:dark:bg-gray-500", | ||
)} | ||
/> | ||
</RadioGroupPrimitives.Indicator> | ||
</div> | ||
) | ||
}) | ||
RadioCardIndicator.displayName = "RadioCardIndicator" | ||
|
||
export { RadioCardGroup, RadioCardIndicator, RadioCardItem } |
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 @@ | ||
# Tremor Raw Radio Card Group Changelog | ||
|
||
## 0.0.0 | ||
|
||
### Changes |
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,77 @@ | ||
import { expect, test } from "@playwright/test" | ||
|
||
test.describe("Expect radiocardgroup default", () => { | ||
test("to be rendered", async ({ page }) => { | ||
await page.goto( | ||
"http://localhost:6006/?path=/story/ui-radiocardgroup--default", | ||
) | ||
await expect( | ||
page | ||
.frameLocator('iframe[title="storybook-preview-iframe"]') | ||
.getByRole("radiogroup"), | ||
).toBeVisible() | ||
}) | ||
test("to render radiogroupitems", async ({ page }) => { | ||
await page.goto( | ||
"http://localhost:6006/?path=/story/ui-radiocardgroup--default", | ||
) | ||
await expect( | ||
page | ||
.frameLocator('iframe[title="storybook-preview-iframe"]') | ||
.getByRole("radiogroup"), | ||
).toBeVisible() | ||
await expect( | ||
page | ||
.frameLocator('iframe[title="storybook-preview-iframe"]') | ||
.getByRole("radio", { name: "Software Engineer" }), | ||
).toBeVisible() | ||
await expect( | ||
page | ||
.frameLocator('iframe[title="storybook-preview-iframe"]') | ||
.getByRole("radio", { name: "Platform Engineer" }), | ||
).toBeVisible() | ||
await expect( | ||
page | ||
.frameLocator('iframe[title="storybook-preview-iframe"]') | ||
.getByRole("radio", { name: "Hardware Engineer" }), | ||
).toBeVisible() | ||
await expect( | ||
page | ||
.frameLocator('iframe[title="storybook-preview-iframe"]') | ||
.getByRole("radio"), | ||
).toHaveCount(3) | ||
}) | ||
|
||
test("to be checkable", async ({ page }) => { | ||
await page.goto( | ||
"http://localhost:6006/?path=/story/ui-radiocardgroup--default", | ||
) | ||
await page | ||
.frameLocator('iframe[title="storybook-preview-iframe"]') | ||
.getByRole("radio", { name: "Platform Engineer" }) | ||
.click() | ||
await expect( | ||
page | ||
.frameLocator('iframe[title="storybook-preview-iframe"]') | ||
.getByRole("radio", { name: "Platform Engineer" }), | ||
).toBeVisible() | ||
await expect( | ||
page | ||
.frameLocator('iframe[title="storybook-preview-iframe"]') | ||
.getByRole("radio", { name: "Platform Engineer" }), | ||
).toBeChecked() | ||
}) | ||
}) | ||
|
||
test.describe("Expect radiogroup disabled", () => { | ||
test("to be disabled", async ({ page }) => { | ||
await page.goto( | ||
"http://localhost:6006/?path=/story/ui-radiocardgroup--disabled", | ||
) | ||
await expect( | ||
page | ||
.frameLocator('iframe[title="storybook-preview-iframe"]') | ||
.getByRole("radio", { name: "Hardware Engineer" }), | ||
).toBeDisabled() | ||
}) | ||
}) |
202 changes: 202 additions & 0 deletions
202
src/components/RadioCardGroup/radiocardgroup.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,202 @@ | ||
import React from "react" | ||
import type { Meta, StoryObj } from "@storybook/react" | ||
|
||
import { Button } from "../Button/Button" | ||
import { | ||
RadioCardGroup, | ||
RadioCardIndicator, | ||
RadioCardItem, | ||
} from "./RadioCardGroup" | ||
|
||
const meta: Meta<typeof RadioCardGroup> = { | ||
title: "ui/RadioCardGroup", | ||
component: RadioCardGroup, | ||
} | ||
|
||
export default meta | ||
type Story = StoryObj<typeof RadioCardGroup> | ||
|
||
export const Default: Story = { | ||
render: () => { | ||
return ( | ||
<RadioCardGroup> | ||
<RadioCardItem value="1"> | ||
<div className="flex items-center gap-3"> | ||
<RadioCardIndicator /> | ||
<span>Software Engineer</span> | ||
</div> | ||
</RadioCardItem> | ||
<RadioCardItem value="2"> | ||
<div className="flex items-center gap-3"> | ||
<RadioCardIndicator /> | ||
<span>Platform Engineer</span> | ||
</div> | ||
</RadioCardItem> | ||
<RadioCardItem value="3"> | ||
<div className="flex items-center gap-3"> | ||
<RadioCardIndicator /> | ||
<span>Hardware Engineer</span> | ||
</div> | ||
</RadioCardItem> | ||
</RadioCardGroup> | ||
) | ||
}, | ||
} | ||
|
||
export const Grid: Story = { | ||
render: () => { | ||
return ( | ||
<RadioCardGroup className="grid-cols-2"> | ||
<RadioCardItem value="1"> | ||
<div className="flex items-center gap-3"> | ||
<RadioCardIndicator /> | ||
<span>Software Engineer</span> | ||
</div> | ||
</RadioCardItem> | ||
<RadioCardItem value="2"> | ||
<div className="flex items-center gap-3"> | ||
<RadioCardIndicator /> | ||
<span>Platform Engineer</span> | ||
</div> | ||
</RadioCardItem> | ||
<RadioCardItem value="3"> | ||
<div className="flex items-center gap-3"> | ||
<RadioCardIndicator /> | ||
<span>Hardware Engineer</span> | ||
</div> | ||
</RadioCardItem> | ||
<RadioCardItem value="4"> | ||
<div className="flex items-center gap-3"> | ||
<RadioCardIndicator /> | ||
<span>Security</span> | ||
</div> | ||
</RadioCardItem> | ||
<RadioCardItem value="5"> | ||
<div className="flex items-center gap-3"> | ||
<RadioCardIndicator /> | ||
<span>Marketing Ops</span> | ||
</div> | ||
</RadioCardItem> | ||
<RadioCardItem value="6"> | ||
<div className="flex items-center gap-3"> | ||
<RadioCardIndicator /> | ||
<span>Product Manager</span> | ||
</div> | ||
</RadioCardItem> | ||
</RadioCardGroup> | ||
) | ||
}, | ||
} | ||
|
||
export const DefaultChecked: Story = { | ||
render: () => { | ||
return ( | ||
<RadioCardGroup defaultValue="1" className="text-sm"> | ||
<RadioCardItem value="1" className="flex items-center gap-3"> | ||
<RadioCardIndicator /> | ||
<span>Software Engineer</span> | ||
</RadioCardItem> | ||
<RadioCardItem value="2" className="flex items-center gap-3"> | ||
<RadioCardIndicator /> | ||
<span>Plarform Engineer</span> | ||
</RadioCardItem> | ||
<RadioCardItem value="3" className="flex items-center gap-3"> | ||
<RadioCardIndicator /> | ||
<span>Hardware Engineer</span> | ||
</RadioCardItem> | ||
</RadioCardGroup> | ||
) | ||
}, | ||
} | ||
|
||
export const Disabled: Story = { | ||
render: () => { | ||
return ( | ||
<RadioCardGroup defaultValue="1" className="text-sm"> | ||
<RadioCardItem value="1" className="flex items-center gap-3"> | ||
<RadioCardIndicator /> | ||
<span>Software Engineer</span> | ||
</RadioCardItem> | ||
<RadioCardItem value="2" className="flex items-center gap-3"> | ||
<RadioCardIndicator /> | ||
<span>Plarform Engineer</span> | ||
</RadioCardItem> | ||
<RadioCardItem disabled value="3" className="flex items-center gap-3"> | ||
<RadioCardIndicator /> | ||
<span>Hardware Engineer</span> | ||
</RadioCardItem> | ||
</RadioCardGroup> | ||
) | ||
}, | ||
} | ||
|
||
export const Controlled: Story = { | ||
render: () => { | ||
const [selectedOption, setSelectedOption] = | ||
React.useState("base-performance") | ||
|
||
const databases: { | ||
label: string | ||
value: string | ||
description: string | ||
isRecommended: boolean | ||
}[] = [ | ||
{ | ||
label: "Base performance", | ||
value: "base-performance", | ||
description: "1/8 vCPU, 1 GB RAM", | ||
isRecommended: true, | ||
}, | ||
{ | ||
label: "Advanced performance", | ||
value: "advanced-performance", | ||
description: "1/4 vCPU, 2 GB RAM", | ||
isRecommended: false, | ||
}, | ||
{ | ||
label: "Turbo performance", | ||
value: "turbo-performance", | ||
description: "1/2 vCPU, 4 GB RAM", | ||
isRecommended: false, | ||
}, | ||
] | ||
|
||
return ( | ||
<div className="flex flex-col items-center justify-start"> | ||
<form> | ||
<fieldset className="space-y-3"> | ||
<RadioCardGroup | ||
value={selectedOption} | ||
onValueChange={(value) => setSelectedOption(value)} | ||
className="mt-2 grid grid-cols-1 gap-4 text-sm md:grid-cols-2" | ||
> | ||
{databases.map((database) => ( | ||
<RadioCardItem key={database.value} value={database.value}> | ||
<div className="flex items-start gap-3"> | ||
<RadioCardIndicator className="mt-1" /> | ||
<div> | ||
<p className="mt-1 text-xs text-gray-500"> | ||
1/8 vCPU, 1 GB RAM | ||
</p> | ||
</div> | ||
</div> | ||
</RadioCardItem> | ||
))} | ||
</RadioCardGroup> | ||
</fieldset> | ||
<Button | ||
className="mt-4" | ||
type="reset" | ||
variant="secondary" | ||
onClick={() => setSelectedOption("base-performance")} | ||
> | ||
Reset | ||
</Button> | ||
</form> | ||
<pre className="mt-6 w-fit rounded-md bg-gray-100 p-2 font-mono text-sm text-gray-700 dark:bg-gray-800 dark:text-gray-200"> | ||
Selected Opt: {selectedOption ? selectedOption : "Nothing selected!"} | ||
</pre> | ||
</div> | ||
) | ||
}, | ||
} |