From cbc22fe16f325f74d3146fc447cf6d48c732fa95 Mon Sep 17 00:00:00 2001
From: Wahyu Kurniawan <ayungavis@gmail.com>
Date: Thu, 22 Feb 2024 01:01:49 +0700
Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20feat:=20create=20radio=20c?=
 =?UTF-8?q?omponent?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../components/shared/Radio/Radio.theme.ts    | 54 ++++++++++++++
 .../src/components/shared/Radio/Radio.tsx     | 63 ++++++++++++++++
 .../src/components/shared/Radio/RadioItem.tsx | 74 +++++++++++++++++++
 .../src/components/shared/Radio/index.ts      |  2 +
 4 files changed, 193 insertions(+)
 create mode 100644 packages/frontend/src/components/shared/Radio/Radio.theme.ts
 create mode 100644 packages/frontend/src/components/shared/Radio/Radio.tsx
 create mode 100644 packages/frontend/src/components/shared/Radio/RadioItem.tsx
 create mode 100644 packages/frontend/src/components/shared/Radio/index.ts

diff --git a/packages/frontend/src/components/shared/Radio/Radio.theme.ts b/packages/frontend/src/components/shared/Radio/Radio.theme.ts
new file mode 100644
index 00000000..0b84601e
--- /dev/null
+++ b/packages/frontend/src/components/shared/Radio/Radio.theme.ts
@@ -0,0 +1,54 @@
+import { VariantProps, tv } from 'tailwind-variants';
+
+export const radioTheme = tv({
+  slots: {
+    root: ['flex', 'gap-3', 'flex-wrap'],
+    wrapper: ['flex', 'items-center', 'gap-2', 'group'],
+    label: ['text-sm', 'tracking-[-0.006em]', 'text-elements-high-em'],
+    radio: [
+      'w-5',
+      'h-5',
+      'rounded-full',
+      'border',
+      'group',
+      'border-border-interactive/10',
+      'shadow-button',
+      'group-hover:border-border-interactive/[0.14]',
+      'focus-ring',
+      // Checked
+      'data-[state=checked]:bg-controls-primary',
+      'data-[state=checked]:group-hover:bg-controls-primary-hovered',
+    ],
+    indicator: [
+      'flex',
+      'items-center',
+      'justify-center',
+      'relative',
+      'w-full',
+      'h-full',
+      'after:content-[""]',
+      'after:block',
+      'after:w-2.5',
+      'after:h-2.5',
+      'after:rounded-full',
+      'after:bg-transparent',
+      'after:group-hover:bg-controls-disabled',
+      'after:group-focus-visible:bg-controls-disabled',
+      // Checked
+      'after:data-[state=checked]:bg-elements-on-primary',
+      'after:data-[state=checked]:group-hover:bg-elements-on-primary',
+      'after:data-[state=checked]:group-focus-visible:bg-elements-on-primary',
+    ],
+  },
+  variants: {
+    orientation: {
+      vertical: { root: ['flex-col'] },
+      horizontal: { root: ['flex-row'] },
+    },
+  },
+  defaultVariants: {
+    orientation: 'vertical',
+  },
+});
+
+export type RadioTheme = VariantProps<typeof radioTheme>;
diff --git a/packages/frontend/src/components/shared/Radio/Radio.tsx b/packages/frontend/src/components/shared/Radio/Radio.tsx
new file mode 100644
index 00000000..96542493
--- /dev/null
+++ b/packages/frontend/src/components/shared/Radio/Radio.tsx
@@ -0,0 +1,63 @@
+import React from 'react';
+import {
+  Root as RadixRoot,
+  RadioGroupProps,
+} from '@radix-ui/react-radio-group';
+import { RadioTheme, radioTheme } from './Radio.theme';
+import { RadioItem, RadioItemProps } from './RadioItem';
+
+export interface RadioOption extends RadioItemProps {
+  /**
+   * The label of the radio option.
+   */
+  label: string;
+  /**
+   * The value of the radio option.
+   */
+  value: string;
+}
+
+export interface RadioProps extends RadioGroupProps, RadioTheme {
+  /**
+   * The options of the radio.
+   * @default []
+   * @example
+   * ```tsx
+   * const options = [
+   *  {
+   *    label: 'Label 1',
+   *    value: '1',
+   *  },
+   *  {
+   *    label: 'Label 2',
+   *    value: '2',
+   *  },
+   *  {
+   *    label: 'Label 3',
+   *    value: '3',
+   *  },
+   * ];
+   * ```
+   */
+  options: RadioOption[];
+}
+
+/**
+ * The Radio component is used to select one option from a list of options.
+ */
+export const Radio = ({
+  className,
+  options,
+  orientation,
+  ...props
+}: RadioProps) => {
+  const { root } = radioTheme({ orientation });
+
+  return (
+    <RadixRoot {...props} className={root({ className })}>
+      {options.map((option) => (
+        <RadioItem key={option.value} {...option} />
+      ))}
+    </RadixRoot>
+  );
+};
diff --git a/packages/frontend/src/components/shared/Radio/RadioItem.tsx b/packages/frontend/src/components/shared/Radio/RadioItem.tsx
new file mode 100644
index 00000000..177af9db
--- /dev/null
+++ b/packages/frontend/src/components/shared/Radio/RadioItem.tsx
@@ -0,0 +1,74 @@
+import React, { ComponentPropsWithoutRef } from 'react';
+import {
+  Item as RadixRadio,
+  Indicator as RadixIndicator,
+  RadioGroupItemProps,
+  RadioGroupIndicatorProps,
+} from '@radix-ui/react-radio-group';
+import { radioTheme } from './Radio.theme';
+
+export interface RadioItemProps extends RadioGroupItemProps {
+  /**
+   * The wrapper props of the radio item.
+   * You can use this prop to customize the wrapper props.
+   */
+  wrapperProps?: ComponentPropsWithoutRef<'div'>;
+  /**
+   * The label props of the radio item.
+   * You can use this prop to customize the label props.
+   */
+  labelProps?: ComponentPropsWithoutRef<'label'>;
+  /**
+   * The indicator props of the radio item.
+   * You can use this prop to customize the indicator props.
+   */
+  indicatorProps?: RadioGroupIndicatorProps;
+  /**
+   * The id of the radio item.
+   */
+  id?: string;
+  /**
+   * The label of the radio item.
+   */
+  label?: string;
+}
+
+/**
+ * The RadioItem component is used to render a radio item.
+ */
+export const RadioItem = ({
+  className,
+  wrapperProps,
+  labelProps,
+  indicatorProps,
+  label,
+  id,
+  ...props
+}: RadioItemProps) => {
+  const { wrapper, label: labelClass, radio, indicator } = radioTheme();
+
+  // Generate a unique id for the radio item from the label if the id is not provided
+  const kebabCaseLabel = label?.toLowerCase().replace(/\s+/g, '-');
+  const componentId = id ?? kebabCaseLabel;
+
+  return (
+    <div className={wrapper({ className: wrapperProps?.className })}>
+      <RadixRadio {...props} className={radio({ className })} id={componentId}>
+        <RadixIndicator
+          forceMount
+          {...indicatorProps}
+          className={indicator({ className: indicatorProps?.className })}
+        />
+      </RadixRadio>
+      {label && (
+        <label
+          {...labelProps}
+          className={labelClass({ className: labelProps?.className })}
+          htmlFor={componentId}
+        >
+          {label}
+        </label>
+      )}
+    </div>
+  );
+};
diff --git a/packages/frontend/src/components/shared/Radio/index.ts b/packages/frontend/src/components/shared/Radio/index.ts
new file mode 100644
index 00000000..6d49f1a8
--- /dev/null
+++ b/packages/frontend/src/components/shared/Radio/index.ts
@@ -0,0 +1,2 @@
+export * from './Radio';
+export * from './RadioItem';