-
Notifications
You must be signed in to change notification settings - Fork 622
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(admin-ui): Radio & RadioGroup component (#4400)
- Loading branch information
Showing
14 changed files
with
607 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}; |
Oops, something went wrong.