From f1fd843af80f1327df99147214c33e588d1e4ad6 Mon Sep 17 00:00:00 2001
From: Hernan Alvarado <hernanvid123@gmail.com>
Date: Fri, 10 Jan 2025 20:15:50 +0000
Subject: [PATCH] feat: add `RadioGroup` component (#76)

---
 packages/ui-radio-group/package.json          | 48 +++++++++++++
 packages/ui-radio-group/src/index.tsx         | 43 ++++++++++++
 .../src/radio-group.stories.tsx               | 68 +++++++++++++++++++
 packages/ui-radio-group/tsconfig.json         |  9 +++
 packages/ui-radio-group/tsup.config.ts        |  4 ++
 packages/ui-radio/src/index.tsx               |  4 +-
 pnpm-lock.yaml                                |  9 +++
 7 files changed, 184 insertions(+), 1 deletion(-)
 create mode 100644 packages/ui-radio-group/package.json
 create mode 100644 packages/ui-radio-group/src/index.tsx
 create mode 100644 packages/ui-radio-group/src/radio-group.stories.tsx
 create mode 100644 packages/ui-radio-group/tsconfig.json
 create mode 100644 packages/ui-radio-group/tsup.config.ts

diff --git a/packages/ui-radio-group/package.json b/packages/ui-radio-group/package.json
new file mode 100644
index 0000000..ad73974
--- /dev/null
+++ b/packages/ui-radio-group/package.json
@@ -0,0 +1,48 @@
+{
+  "name": "@halvaradop/ui-radio-group",
+  "version": "0.1.0",
+  "private": false,
+  "description": "A customizable Radio Group component for @halvaradop/ui library with Tailwind CSS styling.",
+  "type": "module",
+  "scripts": {
+    "dev": "tsup --watch",
+    "build": "tsup",
+    "format": "prettier --write .",
+    "format:check": "prettier --check .",
+    "clean": "pnpm clean:build && pnpm clean:modules",
+    "clean:build": "rm -rf dist",
+    "clean:modules": "rm -rf node_modules"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/halvaradop/ui.git"
+  },
+  "keywords": [
+    "react",
+    "component",
+    "tailwindcss",
+    "react",
+    "ui",
+    "ui library"
+  ],
+  "author": "Hernan Alvarado <hernanvid123@gmail.com>",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/halvaradop/ui/issues"
+  },
+  "homepage": "https://github.com/halvaradop/ui#readme",
+  "files": [
+    "dist"
+  ],
+  "exports": {
+    ".": {
+      "types": "./dist/index.d.ts",
+      "import": "./dist/index.js",
+      "require": "./dist/index.cjs"
+    }
+  },
+  "dependencies": {
+    "@halvaradop/ui-core": "workspace:*",
+    "class-variance-authority": "0.7.0"
+  }
+}
diff --git a/packages/ui-radio-group/src/index.tsx b/packages/ui-radio-group/src/index.tsx
new file mode 100644
index 0000000..dd2445b
--- /dev/null
+++ b/packages/ui-radio-group/src/index.tsx
@@ -0,0 +1,43 @@
+"use client"
+import { ComponentProps, forwardRef, useEffect, useRef } from "react"
+import { merge, type ArgsFunction } from "@halvaradop/ui-core"
+import { cva, type VariantProps } from "class-variance-authority"
+
+export type RadioGroupProps<T extends ArgsFunction> = VariantProps<T> &
+    Omit<ComponentProps<"fieldset">, "children"> & {
+        children: React.ReactNode
+    }
+
+export const radioGroupVariants = cva("flex", {
+    variants: {
+        variant: {
+            row: "flex-row gap-x-5",
+            column: "flex-col gap-y-1",
+        },
+    },
+    defaultVariants: {
+        variant: "column",
+    },
+})
+
+export const RadioGroup = forwardRef<HTMLFieldSetElement, RadioGroupProps<typeof radioGroupVariants>>(({ className, variant, name, defaultValue, children, ...props }, ref) => {
+    // @ts-ignore
+    const reference = useRef<HTMLFieldSetElement>(ref?.current)
+
+    useEffect(() => {
+        if (!reference.current) return
+        const radioButtons = reference.current.querySelectorAll("input[type=radio]")
+        radioButtons.forEach((radio) => {
+            if (radio.getAttribute("value") === defaultValue) {
+                radio.setAttribute("checked", "checked")
+            }
+            radio.setAttribute("name", name ?? "default-radio-group")
+        })
+    }, [])
+
+    return (
+        <fieldset className={merge(radioGroupVariants({ className, variant }))} defaultValue={defaultValue} ref={reference} {...props}>
+            {children}
+        </fieldset>
+    )
+})
diff --git a/packages/ui-radio-group/src/radio-group.stories.tsx b/packages/ui-radio-group/src/radio-group.stories.tsx
new file mode 100644
index 0000000..3569b58
--- /dev/null
+++ b/packages/ui-radio-group/src/radio-group.stories.tsx
@@ -0,0 +1,68 @@
+import type { Meta, StoryObj } from "@storybook/react"
+import { RadioGroup } from "./index.js"
+import { Label } from "../../ui-label/src/index.js"
+import { Radio } from "../../ui-radio/src/index.js"
+
+const meta: Meta = {
+    title: "ui-radio-group",
+    tags: ["autodocs"],
+    component: RadioGroup,
+    parameters: {
+        layout: "centered",
+    },
+} satisfies Meta<typeof RadioGroup>
+
+type Story = StoryObj<typeof meta>
+
+export const Column: Story = {
+    render: () => {
+        return (
+            <RadioGroup name="food">
+                <Label className="flex items-center gap-x-2">
+                    <Radio value="pizza" name="food" />
+                    Pizza
+                </Label>
+                <Label className="flex items-center gap-x-2">
+                    <Radio value="hamburger" name="food" />
+                    Hamburger
+                </Label>
+            </RadioGroup>
+        )
+    },
+}
+
+export const Row: Story = {
+    render: () => {
+        return (
+            <RadioGroup variant="row" name="food">
+                <Label className="flex items-center gap-x-2">
+                    <Radio value="pizza" name="food" />
+                    Pizza
+                </Label>
+                <Label className="flex items-center gap-x-2">
+                    <Radio value="hamburger" name="food" />
+                    Hamburger
+                </Label>
+            </RadioGroup>
+        )
+    },
+}
+
+export const DefaultChecked: Story = {
+    render: () => {
+        return (
+            <RadioGroup name="food" defaultValue="pizza">
+                <Label className="flex items-center gap-x-2">
+                    <Radio value="pizza" name="food" />
+                    Pizza
+                </Label>
+                <Label className="flex items-center gap-x-2">
+                    <Radio value="hamburger" name="food" />
+                    Hamburger
+                </Label>
+            </RadioGroup>
+        )
+    },
+}
+
+export default meta
diff --git a/packages/ui-radio-group/tsconfig.json b/packages/ui-radio-group/tsconfig.json
new file mode 100644
index 0000000..4738e10
--- /dev/null
+++ b/packages/ui-radio-group/tsconfig.json
@@ -0,0 +1,9 @@
+{
+  "extends": "@halvaradop/ui-core/tsconfig.base.json",
+  "compilerOptions": {
+    "outDir": "dist",
+    "jsx": "react-jsx"
+  },
+  "include": ["src"],
+  "exclude": ["node_modules", "dist"]
+}
diff --git a/packages/ui-radio-group/tsup.config.ts b/packages/ui-radio-group/tsup.config.ts
new file mode 100644
index 0000000..75ee2d2
--- /dev/null
+++ b/packages/ui-radio-group/tsup.config.ts
@@ -0,0 +1,4 @@
+import { defineConfig } from "tsup"
+import { tsupConfig } from "@halvaradop/ui-core/tsup.config.base"
+
+export default defineConfig(tsupConfig)
diff --git a/packages/ui-radio/src/index.tsx b/packages/ui-radio/src/index.tsx
index 461f5e4..b10e9d0 100644
--- a/packages/ui-radio/src/index.tsx
+++ b/packages/ui-radio/src/index.tsx
@@ -50,9 +50,11 @@ const internalVariants = cva("block absolute rounded-full", {
 
 export const Radio = ({ className, size, color, name, ...props }: RadioProps<typeof radioVariants & typeof internalVariants>) => {
     return (
-        <label className="flex items-center justify-center relative" htmlFor={name}>
+        <label className="w-min inline-flex items-center justify-center relative" htmlFor={name}>
             <input className={merge(radioVariants({ className, size, color }))} type="radio" name={name} {...props} />
             <span className={internalVariants({ size, color })} />
         </label>
     )
 }
+
+Radio.displayName = "Radio"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1c14097..0bd418e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -160,6 +160,15 @@ importers:
         specifier: 0.7.0
         version: 0.7.0
 
+  packages/ui-radio-group:
+    dependencies:
+      '@halvaradop/ui-core':
+        specifier: workspace:*
+        version: link:../ui-core
+      class-variance-authority:
+        specifier: 0.7.0
+        version: 0.7.0
+
   packages/ui-submit:
     dependencies:
       '@halvaradop/ui-core':