diff --git a/packages/components/src/tailwind/Radio/Radio.stories.tsx b/packages/components/src/tailwind/Radio/Radio.stories.tsx new file mode 100644 index 00000000000..9e3fd60ab33 --- /dev/null +++ b/packages/components/src/tailwind/Radio/Radio.stories.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Radio, RadioGroup, type RadioGroupProps } from './Radio'; + +export default { + title: 'Tailwind/Radio', + component: Radio, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: {}, +}; + +export const Default = (args: RadioGroupProps) => { + return ( +
+ + Apple: Red, Green, Golden + Banana: Ripe, Unripe, Overripe + Grapes: Seedless, With seeds, Dried (Raisin) + Orange: Navel, Blood, Mandarin + Mango: Alphonso, Kesar, Dasheri + +
+ ); +}; + +Default.args = { label: 'Fruits' } as RadioGroupProps; + +// Disabled story +export const Disabled = Default.bind({}); +Disabled.args = { + isDisabled: true, +}; + +// Invalid story +export const Invalid = Default.bind({}); +Invalid.args = { + isInvalid: true, + errorMessage: 'Please select a fruits.', +}; diff --git a/packages/components/src/tailwind/Radio/Radio.tsx b/packages/components/src/tailwind/Radio/Radio.tsx new file mode 100644 index 00000000000..d26e2d8b299 --- /dev/null +++ b/packages/components/src/tailwind/Radio/Radio.tsx @@ -0,0 +1,110 @@ +import React from 'react'; + +import type { + RadioGroupProps as AriaRadioGroupProps, + RadioProps as AriaRadioProps, + ValidationResult, +} from 'react-aria-components'; +import { + RadioGroup as AriaRadioGroup, + Radio as AriaRadio, + composeRenderProps, +} from 'react-aria-components'; +import { tv } from 'tailwind-variants'; + +import { composeTailwindRenderProps } from '../utils'; +import { Description, FieldError, Label } from '../Field/Field'; + +interface RadioGroupProps extends Omit { + label?: string; + children?: React.ReactNode; + description?: string; + errorMessage?: string | ((validation: ValidationResult) => string); + ref?: React.Ref; +} + +const RadioGroup = (props: RadioGroupProps) => { + return ( + + + {props.children} + {props.description && {props.description}} + {props.errorMessage} + + ); +}; + +const radioStyles = tv({ + base: 'bg-muted size-4 shrink-0 rounded-full border transition', + variants: { + isSelected: { + false: 'border-input', + true: 'border-primary border-[4.5px]', + }, + isFocused: { + true: [ + 'border-ring bg-primary/20 ring-primary/20 ring-4', + 'group-invalid:border-danger/70 group-invalid:bg-danger/20 group-invalid:ring-danger/20', + ], + }, + isInvalid: { + true: 'border-danger/70 bg-danger/20 text-red-700', + }, + isDisabled: { + true: 'opacity-50', + }, + }, +}); + +const radioBaseStyles = tv({ + base: 'group flex items-center gap-2 text-sm transition [&[data-disabled]]:opacity-50', +}); + +interface RadioProps extends AriaRadioProps { + description?: string; + label?: string; + ref?: React.Ref; +} + +const Radio = (props: RadioProps) => { + return ( + + radioBaseStyles({ ...renderProps, className }), + )} + > + {(renderProps) => ( +
+
+
+ {props.label || props.description ? ( + <> + {} + {props.description && ( + {props.description} + )} + + ) : ( + (props.children as React.ReactNode) + )} +
+
+ )} + + ); +}; + +export type { RadioGroupProps, RadioProps }; +export { Radio, RadioGroup };