Skip to content

Commit

Permalink
feat(admin-ui): Radio & RadioGroup component (#4400)
Browse files Browse the repository at this point in the history
  • Loading branch information
leopuleo authored Nov 28, 2024
1 parent 54fa286 commit eedd04a
Show file tree
Hide file tree
Showing 14 changed files with 607 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/admin-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.0",
Expand Down
59 changes: 59 additions & 0 deletions packages/admin-ui/src/Radio/Radio.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as React from "react";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { cn, makeDecoratable } from "~/utils";

/**
* RadioItem
*/
interface RadioProps extends React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> {
label: string | React.ReactNode;
id: string;
}

const DecoratableRadio = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
RadioProps
>(({ className, label, id, ...props }, ref) => {
return (
<div className="flex items-start space-x-sm-extra">
<RadioGroupPrimitive.Item
ref={ref}
id={id}
className={cn(
[
"group peer aspect-square h-md w-md rounded-xl mt-xxs",
"bg-neutral-base border-sm border-neutral-muted ring-offset-background",
"focus:outline-none focus-visible:border-accent-default focus-visible:ring-lg focus-visible:ring-primary-dimmed focus-visible:ring-offset-0",
"disabled:cursor-not-allowed disabled:border-neutral-muted disabled:bg-neutral-disabled"
],
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<span
className={cn([
"h-sm w-sm rounded-xl",
"bg-primary-default",
"group-disabled:bg-neutral-strong"
])}
/>
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
<label
htmlFor={id}
className={cn([
"text-md",
"text-neutral-primary",
"peer-disabled:cursor-not-allowed peer-disabled:text-neutral-disabled"
])}
>
{label}
</label>
</div>
);
});
DecoratableRadio.displayName = RadioGroupPrimitive.Item.displayName;
const Radio = makeDecoratable("Radio", DecoratableRadio);

export { Radio, type RadioProps };
215 changes: 215 additions & 0 deletions packages/admin-ui/src/Radio/RadioGroup.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import React, { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { RadioGroup } from "./RadioGroup";
import { Text } from "~/Text";
import { Button } from "~/Button";

const meta: Meta<typeof RadioGroup> = {
title: "Components/RadioGroup",
component: RadioGroup,
tags: ["autodocs"],
parameters: {
layout: "fullscreen"
},
decorators: [
Story => (
<div className="w-[60%] h-48 mx-auto flex justify-center items-center">
<Story />
</div>
)
],
render: args => {
const [value, setValue] = useState(args.value);
return <RadioGroup {...args} value={value} onValueChange={value => setValue(value)} />;
}
};

export default meta;

type Story = StoryObj<typeof RadioGroup>;

export const Default: Story = {
args: {
items: [
{
value: "value-1",
label: "Value 1"
},
{
value: "value-2",
label: "Value 2"
},
{
value: "value-3",
label: "Value 3"
}
]
}
};

export const Disabled: Story = {
args: {
...Default.args,
value: "value-2",
disabled: true,
items: [
{
value: "value-1",
label: "Value 1"
},
{
value: "value-2",
label: "Value 2"
},
{
value: "value-3",
label: "Value 3"
}
]
}
};

export const WithDefaultOption: Story = {
args: {
...Default.args,
value: "value-2"
}
};

export const WithDisabledOption: Story = {
args: {
...Default.args,
items: [
{
value: "value-1",
label: "Value 1"
},
{
value: "value-2",
label: "Value 2",
disabled: true
},
{
value: "value-3",
label: "Value 3"
}
]
}
};

export const WithDefaultDisabledOption: Story = {
args: {
...Default.args,
value: "value-2",
items: [
{
value: "value-1",
label: "Value 1"
},
{
value: "value-2",
label: "Value 2",
disabled: true
},
{
value: "value-3",
label: "Value 3"
}
]
}
};

export const WithLongOption: Story = {
args: {
...Default.args,
items: [
{
value: "value-1",
label: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer fringilla magna vel massa suscipit mollis. Nunc dui felis, iaculis id tortor ut, hendrerit pulvinar felis."
},
{
value: "value-2",
label: " Pellentesque erat ipsum, posuere dapibus diam id, accumsan sagittis mi. In eu nibh ut nunc ultricies placerat.",
disabled: true
},
{
value: "value-3",
label: "Integer a hendrerit dui. Sed tincidunt vel nibh a finibus."
}
]
}
};

export const WithComplexOptions: Story = {
args: {
...Default.args,
items: [
{
id: "value-1",
value: "value-1",
label: (
<div>
<div>{"Value 1"}</div>
<Text text={"Option description 1"} size={"sm"} />
</div>
)
},
{
id: "value-2",
value: "value-2",
label: (
<div>
<div>{"Value 2"}</div>
<Text text={"Option description 2"} size={"sm"} />
</div>
)
},
{
id: "value-3",
value: "value-3",
label: (
<div>
<div>{"Value 3"}</div>
<Text text={"Option description 3"} size={"sm"} />
</div>
)
}
]
}
};

export const WithExternalValueControl: Story = {
args: {
value: "value-2",
items: [
{
value: "value-1",
label: "Value 1"
},
{
value: "value-2",
label: "Value 2"
},
{
value: "value-3",
label: "Value 3"
}
]
},
render: args => {
const [value, setValue] = useState(args.value);
return (
<div className={"w-full"}>
<div>
<RadioGroup {...args} value={value} onValueChange={value => setValue(value)} />
</div>
<div className={"mt-4 text-center"}>
<Button onClick={() => setValue(args.value)} text={"Reset"} />
</div>
<div className={"mt-4 text-center"}>
Current selected value: <pre>{value}</pre>
</div>
</div>
);
}
};
93 changes: 93 additions & 0 deletions packages/admin-ui/src/Radio/RadioGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import * as React from "react";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { cn, makeDecoratable } from "~/utils";
import { Radio } from "./Radio";
import { RadioItemParams } from "./RadioItemParams";
import { RadioItemFormatted } from "./RadioItemFormatted";
import { useRadioGroup } from "./useRadioGroup";

/**
* Radio Group Root
*/
const DecoratableRadioGroupRoot = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-sm-extra", className)}
{...props}
ref={ref}
/>
);
});
DecoratableRadioGroupRoot.displayName = RadioGroupPrimitive.Root.displayName;
const RadioGroupRoot = makeDecoratable("RadioGroupRoot", DecoratableRadioGroupRoot);

/**
* Radio Group Renderer
*/
interface RadioGroupProps
extends Omit<RadioGroupPrimitive.RadioGroupProps, "defaultValue" | "onValueChange"> {
items: RadioItemParams[];
onValueChange: (value: string) => void;
}

interface RadioGroupVm {
items: RadioItemFormatted[];
}

interface RadioGroupRendererProps extends Omit<RadioGroupProps, "onValueChange"> {
items: RadioItemFormatted[];
changeValue: (value: string) => void;
}

const DecoratableRadioGroupRenderer = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
RadioGroupRendererProps
>(({ items, changeValue, ...props }, ref) => {
return (
<RadioGroupRoot {...props} ref={ref} onValueChange={value => changeValue(value)}>
{items.map(item => (
<Radio
id={item.id}
value={item.value}
key={item.id}
label={item.label}
disabled={item.disabled}
/>
))}
</RadioGroupRoot>
);
});
DecoratableRadioGroupRenderer.displayName = "DecoratableRadioGroupRenderer";
const RadioGroupRenderer = makeDecoratable("RadioGroupRenderer", DecoratableRadioGroupRenderer);

/**
* Radio Group
*/
const DecoratableRadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
RadioGroupProps
>((props, ref) => {
const { vm, changeValue } = useRadioGroup(props);
return <RadioGroupRenderer {...props} items={vm.items} changeValue={changeValue} ref={ref} />;
});
DecoratableRadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
const RadioGroup = makeDecoratable("RadioGroup", DecoratableRadioGroup);

/**
* @deprecated
* This component is retained in @webiny/admin-ui and used in @webiny/ui solely as a compatibility layer.
* It is marked as deprecated because nesting `Radio` components inside `RadioGroup` is no longer the recommended approach.
* Instead, we now pass an array of `RadioItemDto` directly to `RadioGroup` for better maintainability.
*/
const DeprecatedRadioGroup = makeDecoratable("DeprecatedRadioGroup", RadioGroupRoot);

export {
RadioGroup,
RadioGroupRenderer,
DeprecatedRadioGroup,
type RadioGroupProps,
type RadioGroupVm
};
Loading

0 comments on commit eedd04a

Please sign in to comment.