Skip to content

Commit

Permalink
feat: RadioCardGroup component (#34)
Browse files Browse the repository at this point in the history
* first draft radio cards

* update radio card

* add tests

* update licencse
  • Loading branch information
severinlandolt authored Jun 12, 2024
1 parent 9d2ba1e commit e6acc50
Show file tree
Hide file tree
Showing 5 changed files with 383 additions and 0 deletions.
1 change: 1 addition & 0 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ The following files use parts of:
`@components/Input/Input.tsx`
`@components/Popover/Popover.tsx`
`@components/RadioGroup/RadioGroup.tsx`
`@components/RadioCardGroup/RadioCardGroup.tsx`
`@components/Select/Select.tsx`
`@components/Switch/Switch.tsx`
`@components/Tabs/Tabs.tsx`
Expand Down
98 changes: 98 additions & 0 deletions src/components/RadioCardGroup/RadioCardGroup.tsx
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 }
5 changes: 5 additions & 0 deletions src/components/RadioCardGroup/changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Tremor Raw Radio Card Group Changelog

## 0.0.0

### Changes
77 changes: 77 additions & 0 deletions src/components/RadioCardGroup/radiocardgroup.spec.ts
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 src/components/RadioCardGroup/radiocardgroup.stories.tsx
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>
)
},
}

0 comments on commit e6acc50

Please sign in to comment.