mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
449 lines
9.6 KiB
TypeScript
449 lines
9.6 KiB
TypeScript
import type {ValidationResult} from "@react-types/shared";
|
|
import type {Meta} from "@storybook/react";
|
|
import type {RadioProps, RadioGroupProps} from "../src";
|
|
|
|
import React from "react";
|
|
import {VisuallyHidden} from "@react-aria/visually-hidden";
|
|
import {radio, button, cn} from "@heroui/theme";
|
|
import {clsx} from "@heroui/shared-utils";
|
|
import {Form} from "@heroui/form";
|
|
|
|
import {RadioGroup, Radio, useRadio} from "../src";
|
|
|
|
export default {
|
|
title: "Components/RadioGroup",
|
|
component: RadioGroup,
|
|
onChange: {action: "changed"},
|
|
argTypes: {
|
|
color: {
|
|
control: {
|
|
type: "select",
|
|
},
|
|
options: ["default", "primary", "secondary", "success", "warning", "danger"],
|
|
},
|
|
size: {
|
|
control: {
|
|
type: "select",
|
|
},
|
|
options: ["sm", "md", "lg"],
|
|
},
|
|
isDisabled: {
|
|
control: {
|
|
type: "boolean",
|
|
},
|
|
},
|
|
validationBehavior: {
|
|
control: {
|
|
type: "select",
|
|
},
|
|
options: ["aria", "native"],
|
|
},
|
|
},
|
|
} as Meta<typeof RadioGroup>;
|
|
|
|
const defaultProps = {
|
|
...radio.defaultVariants,
|
|
label: "Options",
|
|
};
|
|
|
|
const Template = (args: RadioGroupProps) => {
|
|
const radioProps = args.description
|
|
? {
|
|
a: {
|
|
description: "Description for Option A",
|
|
},
|
|
b: {
|
|
description: "Description for Option B",
|
|
},
|
|
c: {
|
|
description: "Description for Option C",
|
|
},
|
|
d: {
|
|
description: "Description for Option D",
|
|
},
|
|
}
|
|
: {
|
|
a: {},
|
|
b: {},
|
|
c: {},
|
|
d: {},
|
|
};
|
|
|
|
const items = (
|
|
<>
|
|
<Radio value="A" {...radioProps.a}>
|
|
Option A
|
|
</Radio>
|
|
<Radio value="B" {...radioProps.b}>
|
|
Option B
|
|
</Radio>
|
|
<Radio value="C" {...radioProps.c}>
|
|
Option C
|
|
</Radio>
|
|
<Radio value="D" {...radioProps.d}>
|
|
Option D
|
|
</Radio>
|
|
</>
|
|
);
|
|
|
|
return args.isRequired ? (
|
|
<form
|
|
className="flex flex-col items-start gap-2"
|
|
onSubmit={(e) => {
|
|
alert(`Submitted value: ${e.target["sample"].value}`);
|
|
e.preventDefault();
|
|
}}
|
|
>
|
|
<RadioGroup {...args} name="sample">
|
|
{items}
|
|
</RadioGroup>
|
|
<button className={button({color: "primary"})} type="submit">
|
|
Submit
|
|
</button>
|
|
</form>
|
|
) : (
|
|
<RadioGroup {...args}>{items}</RadioGroup>
|
|
);
|
|
};
|
|
|
|
const InvalidTemplate = (args: RadioGroupProps) => {
|
|
const [isInvalid, setIsInvalid] = React.useState<boolean>(true);
|
|
|
|
const radioProps = args.description
|
|
? {
|
|
a: {
|
|
description: "Description for Option A",
|
|
},
|
|
b: {
|
|
description: "Description for Option B",
|
|
},
|
|
c: {
|
|
description: "Description for Option C",
|
|
},
|
|
d: {
|
|
description: "Description for Option D",
|
|
},
|
|
}
|
|
: {
|
|
a: {},
|
|
b: {},
|
|
c: {},
|
|
d: {},
|
|
};
|
|
|
|
const items = (
|
|
<>
|
|
<Radio value="A" {...radioProps.a}>
|
|
Option A (Invalid)
|
|
</Radio>
|
|
<Radio value="B" {...radioProps.b}>
|
|
Option B (Valid)
|
|
</Radio>
|
|
<Radio value="C" {...radioProps.c}>
|
|
Option C (Valid)
|
|
</Radio>
|
|
<Radio value="D" {...radioProps.d}>
|
|
Option D (Invalid)
|
|
</Radio>
|
|
</>
|
|
);
|
|
|
|
const validOptions = ["C", "B"];
|
|
|
|
return args.isRequired ? (
|
|
<form
|
|
className="flex flex-col items-start gap-2"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
alert("Submitted!");
|
|
}}
|
|
>
|
|
<RadioGroup
|
|
{...args}
|
|
isInvalid={isInvalid}
|
|
onValueChange={(value) => setIsInvalid(!validOptions.includes(value))}
|
|
>
|
|
{items}
|
|
</RadioGroup>
|
|
<button className={button({color: "primary"})} type="submit">
|
|
Submit
|
|
</button>
|
|
</form>
|
|
) : (
|
|
<RadioGroup {...args}>{items}</RadioGroup>
|
|
);
|
|
};
|
|
|
|
const ControlledTemplate = (args: RadioGroupProps) => {
|
|
const [selectedItem, setSelectedItem] = React.useState<string>("london");
|
|
|
|
React.useEffect(() => {
|
|
// eslint-disable-next-line no-console
|
|
console.log("isSelected:", selectedItem);
|
|
}, [selectedItem]);
|
|
|
|
return (
|
|
<div className="flex flex-col gap-2">
|
|
<RadioGroup
|
|
label="Select city"
|
|
value={selectedItem}
|
|
onValueChange={setSelectedItem}
|
|
{...args}
|
|
>
|
|
<Radio value="buenos-aires">Buenos Aires</Radio>
|
|
<Radio value="sydney">Sydney</Radio>
|
|
<Radio value="london">London</Radio>
|
|
<Radio value="tokyo">Tokyo</Radio>
|
|
</RadioGroup>
|
|
<p className="text-default-500">Selected: {selectedItem}</p>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const ServerValidationTemplate = (args: RadioGroupProps) => {
|
|
const [serverErrors, setServerErrors] = React.useState({});
|
|
const onSubmit = (e) => {
|
|
e.preventDefault();
|
|
setServerErrors({
|
|
option: "You must choose an option.",
|
|
});
|
|
};
|
|
|
|
delete args.isInvalid;
|
|
|
|
return (
|
|
<Form
|
|
className="flex flex-col items-start gap-2"
|
|
validationErrors={serverErrors}
|
|
onSubmit={onSubmit}
|
|
>
|
|
<RadioGroup {...args} label="Choose one option" name="option">
|
|
<Radio value="option1">Option 1</Radio>
|
|
<Radio value="option2">Option 2</Radio>
|
|
<Radio value="option3">Option 3</Radio>
|
|
</RadioGroup>
|
|
<button className={button({color: "primary"})} type="submit">
|
|
Submit
|
|
</button>
|
|
</Form>
|
|
);
|
|
};
|
|
|
|
export const Default = {
|
|
render: Template,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
},
|
|
};
|
|
|
|
export const IsDisabled = {
|
|
render: Template,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
isDisabled: true,
|
|
},
|
|
};
|
|
|
|
export const DefaultChecked = {
|
|
render: Template,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
defaultValue: "C",
|
|
},
|
|
};
|
|
|
|
export const IsRequired = {
|
|
render: Template,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
isRequired: true,
|
|
},
|
|
};
|
|
|
|
export const WithDescription = {
|
|
render: Template,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
description: "Please select an option",
|
|
},
|
|
};
|
|
|
|
export const IsInvalid = {
|
|
render: InvalidTemplate,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
isRequired: true,
|
|
description: "Please select an option",
|
|
},
|
|
};
|
|
|
|
export const WithErrorMessage = {
|
|
render: Template,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
isRequired: true,
|
|
isInvalid: true,
|
|
errorMessage: "The selected option is invalid",
|
|
},
|
|
};
|
|
|
|
export const WithErrorMessageFunction = {
|
|
render: Template,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
isRequired: true,
|
|
errorMessage: (value: ValidationResult) => {
|
|
if (value.validationDetails.valueMissing) {
|
|
return "Please select an option";
|
|
}
|
|
},
|
|
},
|
|
};
|
|
|
|
export const WithValidation = {
|
|
render: Template,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
isRequired: true,
|
|
description: "Please select an option",
|
|
validate: (value: string) => {
|
|
if (value === "A") {
|
|
return "Option A is not allowed";
|
|
}
|
|
},
|
|
},
|
|
};
|
|
|
|
export const WithServerValidation = {
|
|
render: ServerValidationTemplate,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
},
|
|
};
|
|
|
|
export const Row = {
|
|
render: Template,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
orientation: "horizontal",
|
|
},
|
|
};
|
|
|
|
export const DisableAnimation = {
|
|
render: Template,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
disableAnimation: true,
|
|
},
|
|
};
|
|
|
|
export const Controlled = {
|
|
render: ControlledTemplate,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
},
|
|
};
|
|
|
|
const CustomRadio = (props: RadioProps) => {
|
|
const {children, ...otherProps} = props;
|
|
|
|
return (
|
|
<Radio
|
|
{...otherProps}
|
|
classNames={{
|
|
base: cn(
|
|
"inline-flex m-0 bg-content1 hover:bg-content2 items-center justify-between",
|
|
"flex-row-reverse max-w-[300px] cursor-pointer rounded-lg gap-4 p-4 border-2 border-transparent",
|
|
"data-[selected=true]:border-primary",
|
|
),
|
|
}}
|
|
>
|
|
{children}
|
|
</Radio>
|
|
);
|
|
};
|
|
|
|
export const CustomWithClassNames = () => {
|
|
return (
|
|
<RadioGroup label="Plans">
|
|
<CustomRadio description="Up to 20 items" value="free">
|
|
Free
|
|
</CustomRadio>
|
|
<CustomRadio description="Unlimited items. $10 per month." value="pro">
|
|
Pro
|
|
</CustomRadio>
|
|
<CustomRadio description="24/7 support. Contact us for pricing." value="enterprise">
|
|
Enterprise
|
|
</CustomRadio>
|
|
</RadioGroup>
|
|
);
|
|
};
|
|
|
|
const RadioCard = (props: RadioProps) => {
|
|
const {
|
|
Component,
|
|
children,
|
|
description,
|
|
getBaseProps,
|
|
getWrapperProps,
|
|
getInputProps,
|
|
getLabelProps,
|
|
getLabelWrapperProps,
|
|
getControlProps,
|
|
} = useRadio(props);
|
|
|
|
return (
|
|
<Component
|
|
{...getBaseProps({
|
|
className: cn(
|
|
"group inline-flex items-center hover:opacity-70 active:opacity-50 justify-between flex-row-reverse tap-highlight-transparent m-0",
|
|
"max-w-[300px] cursor-pointer border-2 border-default rounded-lg gap-4 p-4",
|
|
"data-[selected=true]:border-primary",
|
|
),
|
|
})}
|
|
>
|
|
<VisuallyHidden>
|
|
<input {...getInputProps()} />
|
|
</VisuallyHidden>
|
|
<span {...getWrapperProps()}>
|
|
<span {...getControlProps()} />
|
|
</span>
|
|
<div {...getLabelWrapperProps()}>
|
|
{children && <span {...getLabelProps()}>{children}</span>}
|
|
{description && (
|
|
<span className={clsx("text-sm text-foreground opacity-70")}>{description}</span>
|
|
)}
|
|
</div>
|
|
</Component>
|
|
);
|
|
};
|
|
|
|
export const CustomWithHooks = () => {
|
|
return (
|
|
<RadioGroup label="Plans">
|
|
<RadioCard description="Up to 20 items" value="free">
|
|
Free
|
|
</RadioCard>
|
|
<RadioCard description="Unlimited items. $10 per month." value="pro">
|
|
Pro
|
|
</RadioCard>
|
|
<RadioCard description="24/7 support. Contact us for pricing." value="enterprise">
|
|
Enterprise
|
|
</RadioCard>
|
|
</RadioGroup>
|
|
);
|
|
};
|