feat(radio-group): intial structure

This commit is contained in:
Junior Garcia 2023-03-05 11:07:53 -03:00
parent aefad6676a
commit d57bd1653d
18 changed files with 917 additions and 766 deletions

View File

@ -84,7 +84,7 @@ export function useCheckboxGroup(props: UseCheckboxGroupProps) {
const {labelProps, groupProps} = useReactAriaCheckboxGroup(
{
"aria-label": typeof label === "string" ? label : "Checkbox Group",
"aria-label": typeof label === "string" ? label : otherProps["aria-label"],
...otherProps,
},
groupState,

View File

@ -24,15 +24,15 @@ interface Props extends HTMLNextUIProps<"label"> {
* Ref to the DOM node.
*/
ref?: Ref<HTMLElement>;
/**
* The label of the checkbox.
*/
children?: ReactNode;
/**
* Whether the checkbox is disabled.
* @default false
*/
isDisabled?: boolean;
/**
* The label of the checkbox.
*/
children?: ReactNode;
/**
* The icon to be displayed when the checkbox is checked.
*/
@ -117,6 +117,7 @@ export function useCheckbox(props: UseCheckboxProps) {
isRequired,
onChange,
"aria-label": arialabel,
"aria-labelledby": otherProps["aria-labelledby"] || arialabel,
};
}, [isIndeterminate, isDisabled]);
@ -154,9 +155,8 @@ export function useCheckbox(props: UseCheckboxProps) {
isDisabled,
isFocusVisible,
disableAnimation,
className,
}),
[color, size, radius, lineThrough, isDisabled, isFocusVisible, disableAnimation, className],
[color, size, radius, lineThrough, isDisabled, isFocusVisible, disableAnimation],
);
const baseStyles = clsx(styles?.base, className);

View File

@ -37,8 +37,8 @@
"react": ">=18"
},
"dependencies": {
"@nextui-org/theme": "workspace:*",
"@nextui-org/dom-utils": "workspace:*",
"@nextui-org/shared-css": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/system": "workspace:*",
"@react-aria/focus": "^3.11.0",

View File

@ -1,5 +1,13 @@
import Radio from "./radio";
import RadioGroup from "./radio-group";
// export types
export type {RadioProps} from "./radio";
export type {RadioGroupProps} from "./radio-group";
// export hooks
export {useRadio} from "./use-radio";
export {useRadioGroup} from "./use-radio-group";
// export component
export {default as Radio} from "./radio";
export {Radio, RadioGroup};

View File

@ -1,61 +0,0 @@
import {styled} from "@nextui-org/system";
import {StyledRadio} from "./radio.styles";
export const StyledRadioGroupContainer = styled("div", {
display: "flex",
flexDirection: "column",
variants: {
isRow: {
true: {
mt: 0,
flexDirection: "row",
[`& ${StyledRadio}:not(:last-child)`]: {
mr: "$$radioSize",
},
},
false: {
mr: 0,
flexDirection: "column",
[`& ${StyledRadio}:not(:first-child)`]: {
mt: "$$radioSize",
},
},
},
},
});
export const StyledRadioGroup = styled("div", {
border: 0,
margin: 0,
padding: 0,
display: "flex",
fd: "column",
variants: {
size: {
xs: {
$$radioGroupGap: "$space$7",
},
sm: {
$$radioGroupGap: "$space$8",
},
md: {
$$radioGroupGap: "$space$9",
},
lg: {
$$radioGroupGap: "$space$10",
},
xl: {
$$radioGroupGap: "$space$11",
},
},
},
});
export const StyledRadioGroupLabel = styled("label", {
d: "block",
fontWeight: "$normal",
fontSize: "calc($$checkboxSize * 0.9)",
color: "$accents8",
mb: "$3",
});

View File

@ -1,43 +1,22 @@
import {forwardRef} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx, __DEV__} from "@nextui-org/shared-utils";
import {mergeProps} from "@react-aria/utils";
import {__DEV__} from "@nextui-org/shared-utils";
import {RadioGroupProvider} from "./radio-group-context";
import {
StyledRadioGroup,
StyledRadioGroupLabel,
StyledRadioGroupContainer,
} from "./radio-group.styles";
import {UseRadioGroupProps, useRadioGroup} from "./use-radio-group";
export interface RadioGroupProps extends UseRadioGroupProps {}
export interface RadioGroupProps extends Omit<UseRadioGroupProps, "ref"> {}
const RadioGroup = forwardRef<RadioGroupProps, "div">((props, ref) => {
const {className, children, orientation, label, context, groupProps, labelProps, ...otherProps} =
useRadioGroup(props);
const domRef = useDOMRef(ref);
const {Component, children, label, context, getGroupProps, getLabelProps, getWrapperProps} =
useRadioGroup({ref, ...props});
return (
<StyledRadioGroup
ref={domRef}
className={clsx("nextui-radio-group", className)}
{...mergeProps(groupProps, otherProps)}
>
{label && (
<StyledRadioGroupLabel className="nextui-radio-group-label" {...labelProps}>
{label}
</StyledRadioGroupLabel>
)}
<StyledRadioGroupContainer
className="nextui-radio-group-items"
isRow={orientation === "horizontal"}
role="presentation"
>
<Component {...getGroupProps()}>
{label && <label {...getLabelProps()}>{label}</label>}
<div {...getWrapperProps()}>
<RadioGroupProvider value={context}>{children}</RadioGroupProvider>
</StyledRadioGroupContainer>
</StyledRadioGroup>
</div>
</Component>
);
});
@ -45,6 +24,4 @@ if (__DEV__) {
RadioGroup.displayName = "NextUI.RadioGroup";
}
RadioGroup.toString = () => ".nextui-radio-group";
export default RadioGroup;

View File

@ -1,245 +1,245 @@
import {styled} from "@nextui-org/system";
import {cssFocusVisible} from "@nextui-org/shared-css";
// import {styled} from "@nextui-org/system";
// import {cssFocusVisible} from "@nextui-org/shared-css";
export const StyledRadioText = styled("span", {
fontSize: "$$radioSize",
us: "none",
d: "inline-flex",
ai: "center",
variants: {
color: {
default: {
color: "$text",
},
primary: {
color: "$primary",
},
secondary: {
color: "$secondary",
},
success: {
color: "$success",
},
warning: {
color: "$warning",
},
error: {
color: "$error",
},
},
isDisabled: {
true: {
color: "$accents5",
},
},
isInvalid: {
true: {
color: "$error",
},
},
},
});
// export const StyledRadioText = styled("span", {
// fontSize: "$$radioSize",
// us: "none",
// d: "inline-flex",
// ai: "center",
// variants: {
// color: {
// default: {
// color: "$text",
// },
// primary: {
// color: "$primary",
// },
// secondary: {
// color: "$secondary",
// },
// success: {
// color: "$success",
// },
// warning: {
// color: "$warning",
// },
// error: {
// color: "$error",
// },
// },
// isDisabled: {
// true: {
// color: "$accents5",
// },
// },
// isInvalid: {
// true: {
// color: "$error",
// },
// },
// },
// });
export const StyledRadioPoint = styled(
"span",
{
size: "$$radioSize",
br: "$$radioRadii",
position: "relative",
d: "inline-block",
mr: "calc($$radioSize * 0.375)",
"&:after": {
content: "",
d: "block",
position: "absolute",
size: "$$radioSize",
br: "$$radioRadii",
boxSizing: "border-box",
border: "2px solid $border",
},
},
cssFocusVisible,
);
// export const StyledRadioPoint = styled(
// "span",
// {
// size: "$$radioSize",
// br: "$$radioRadii",
// position: "relative",
// d: "inline-block",
// mr: "calc($$radioSize * 0.375)",
// "&:after": {
// content: "",
// d: "block",
// position: "absolute",
// size: "$$radioSize",
// br: "$$radioRadii",
// boxSizing: "border-box",
// border: "2px solid $border",
// },
// },
// cssFocusVisible,
// );
export const StyledRadio = styled("label", {
d: "flex",
w: "initial",
ai: "flex-start",
position: "relative",
fd: "column",
jc: "flex-start",
cursor: "pointer",
"@motion": {
[`& ${StyledRadioPoint}`]: {
transition: "none",
"&:after": {
transition: "none",
},
},
},
variants: {
color: {
default: {
$$radioColor: "$colors$primary",
$$radioColorHover: "$colors$primarySolidHover",
},
primary: {
$$radioColor: "$colors$primary",
$$radioColorHover: "$colors$primarySolidHover",
},
secondary: {
$$radioColor: "$colors$secondary",
$$radioColorHover: "$colors$secondarySolidHover",
},
success: {
$$radioColor: "$colors$success",
$$radioColorHover: "$colors$successSolidHover",
},
warning: {
$$radioColor: "$colors$warning",
$$radioColorHover: "$colors$warningSolidHover",
},
error: {
$$radioColor: "$colors$error",
$$radioColorHover: "$colors$errorSolidHover",
},
},
size: {
xs: {
$$radioSize: "$space$7",
},
sm: {
$$radioSize: "$space$8",
},
md: {
$$radioSize: "$space$9",
},
lg: {
$$radioSize: "$space$10",
},
xl: {
$$radioSize: "$space$11",
},
},
isHovered: {
true: {},
},
isInvalid: {
true: {
$$radioColor: "$colors$error",
$$radioColorHover: "$colors$errorSolidHover",
[`& ${StyledRadioPoint}`]: {
"&:after": {
borderColor: "$colors$error",
},
},
},
},
isDisabled: {
true: {
cursor: "not-allowed",
$$radioColor: "$colors$accents4",
},
},
isSquared: {
true: {
$$radioRadii: "$radii$squared",
},
false: {
$$radioRadii: "$radii$rounded",
},
},
isChecked: {
true: {
[`& ${StyledRadioPoint}`]: {
"&:after": {
border: "calc($$radioSize * 0.34) solid $$radioColor",
},
},
},
},
disableAnimation: {
true: {
[`& ${StyledRadioPoint}`]: {
transition: "none",
"&:after": {
transition: "none",
},
},
},
false: {
[`& ${StyledRadioPoint}`]: {
transition: "$default",
"&:after": {
transition: "$default",
},
},
},
},
},
compoundVariants: [
// isChecked && isHovered
{
isChecked: true,
isHovered: true,
css: {
[`& ${StyledRadioPoint}`]: {
"&:after": {
border: "calc($$radioSize * 0.34) solid $$radioColorHover",
},
},
},
},
// isChecked && isDisabled & isHovered
{
isChecked: true,
isDisabled: true,
isHovered: true,
css: {
[`& ${StyledRadioPoint}`]: {
"&:after": {
border: "calc($$radioSize * 0.34) solid $$radioColor",
},
},
},
},
// !isChecked && !isDisabled && isHovered
{
isChecked: false,
isDisabled: false,
isHovered: true,
css: {
[`& ${StyledRadioPoint}`]: {
bg: "$border",
},
},
},
],
});
// export const StyledRadio = styled("label", {
// d: "flex",
// w: "initial",
// ai: "flex-start",
// position: "relative",
// fd: "column",
// jc: "flex-start",
// cursor: "pointer",
// "@motion": {
// [`& ${StyledRadioPoint}`]: {
// transition: "none",
// "&:after": {
// transition: "none",
// },
// },
// },
// variants: {
// color: {
// default: {
// $$radioColor: "$colors$primary",
// $$radioColorHover: "$colors$primarySolidHover",
// },
// primary: {
// $$radioColor: "$colors$primary",
// $$radioColorHover: "$colors$primarySolidHover",
// },
// secondary: {
// $$radioColor: "$colors$secondary",
// $$radioColorHover: "$colors$secondarySolidHover",
// },
// success: {
// $$radioColor: "$colors$success",
// $$radioColorHover: "$colors$successSolidHover",
// },
// warning: {
// $$radioColor: "$colors$warning",
// $$radioColorHover: "$colors$warningSolidHover",
// },
// error: {
// $$radioColor: "$colors$error",
// $$radioColorHover: "$colors$errorSolidHover",
// },
// },
// size: {
// xs: {
// $$radioSize: "$space$7",
// },
// sm: {
// $$radioSize: "$space$8",
// },
// md: {
// $$radioSize: "$space$9",
// },
// lg: {
// $$radioSize: "$space$10",
// },
// xl: {
// $$radioSize: "$space$11",
// },
// },
// isHovered: {
// true: {},
// },
// isInvalid: {
// true: {
// $$radioColor: "$colors$error",
// $$radioColorHover: "$colors$errorSolidHover",
// [`& ${StyledRadioPoint}`]: {
// "&:after": {
// borderColor: "$colors$error",
// },
// },
// },
// },
// isDisabled: {
// true: {
// cursor: "not-allowed",
// $$radioColor: "$colors$accents4",
// },
// },
// isSquared: {
// true: {
// $$radioRadii: "$radii$squared",
// },
// false: {
// $$radioRadii: "$radii$rounded",
// },
// },
// isChecked: {
// true: {
// [`& ${StyledRadioPoint}`]: {
// "&:after": {
// border: "calc($$radioSize * 0.34) solid $$radioColor",
// },
// },
// },
// },
// disableAnimation: {
// true: {
// [`& ${StyledRadioPoint}`]: {
// transition: "none",
// "&:after": {
// transition: "none",
// },
// },
// },
// false: {
// [`& ${StyledRadioPoint}`]: {
// transition: "$default",
// "&:after": {
// transition: "$default",
// },
// },
// },
// },
// },
// compoundVariants: [
// // isChecked && isHovered
// {
// isChecked: true,
// isHovered: true,
// css: {
// [`& ${StyledRadioPoint}`]: {
// "&:after": {
// border: "calc($$radioSize * 0.34) solid $$radioColorHover",
// },
// },
// },
// },
// // isChecked && isDisabled & isHovered
// {
// isChecked: true,
// isDisabled: true,
// isHovered: true,
// css: {
// [`& ${StyledRadioPoint}`]: {
// "&:after": {
// border: "calc($$radioSize * 0.34) solid $$radioColor",
// },
// },
// },
// },
// // !isChecked && !isDisabled && isHovered
// {
// isChecked: false,
// isDisabled: false,
// isHovered: true,
// css: {
// [`& ${StyledRadioPoint}`]: {
// bg: "$border",
// },
// },
// },
// ],
// });
export const StyledRadioDescription = styled("span", {
color: "$accents7",
fontSize: "calc($$radioSize * 0.85)",
paddingLeft: "calc($$radioSize + $$radioSize * 0.375)",
variants: {
isInvalid: {
true: {
color: "$red500",
},
},
isDisabled: {
true: {
color: "$accents5",
},
},
},
});
// export const StyledRadioDescription = styled("span", {
// color: "$accents7",
// fontSize: "calc($$radioSize * 0.85)",
// paddingLeft: "calc($$radioSize + $$radioSize * 0.375)",
// variants: {
// isInvalid: {
// true: {
// color: "$red500",
// },
// },
// isDisabled: {
// true: {
// color: "$accents5",
// },
// },
// },
// });
export const StyledRadioContainer = styled("div", {
w: "initial",
position: "relative",
d: "flex",
fd: "row",
ai: "center",
jc: "flex-start",
});
// export const StyledRadioContainer = styled("div", {
// w: "initial",
// position: "relative",
// d: "flex",
// fd: "row",
// ai: "center",
// jc: "flex-start",
// });

View File

@ -1,108 +1,42 @@
import {forwardRef} from "@nextui-org/system";
import {useFocusableRef} from "@nextui-org/dom-utils";
import {clsx, __DEV__} from "@nextui-org/shared-utils";
import {mergeProps} from "@react-aria/utils";
import {__DEV__} from "@nextui-org/shared-utils";
import {VisuallyHidden} from "@react-aria/visually-hidden";
import RadioGroup from "./radio-group";
import {
StyledRadio,
StyledRadioContainer,
StyledRadioPoint,
StyledRadioText,
StyledRadioDescription,
} from "./radio.styles";
import {UseRadioProps, useRadio} from "./use-radio";
export interface RadioProps extends UseRadioProps {}
type CompoundRadio = {
Group: typeof RadioGroup;
};
const Radio = forwardRef<RadioProps, "label", CompoundRadio>((props, ref) => {
const {className, as, css, children, label, description, ...radioProps} = props;
export interface RadioProps extends Omit<UseRadioProps, "ref"> {}
const Radio = forwardRef<RadioProps, "label">((props, ref) => {
const {
size,
state,
color,
inputRef,
labelColor,
isHovered,
isSquared,
isInvalid,
isDisabled,
disableAnimation,
isFocusVisible,
focusProps,
hoverProps,
inputProps,
isRequired,
} = useRadio({...radioProps, children: children ?? label});
const domRef = useFocusableRef(ref, inputRef);
Component,
children,
slots,
styles,
description,
getBaseProps,
getWrapperProps,
getInputProps,
getLabelProps,
} = useRadio({ref, ...props});
return (
<StyledRadio
ref={domRef}
{...hoverProps}
as={as}
className={clsx("nextui-radio", className)}
color={color}
css={css}
data-state={state}
disableAnimation={disableAnimation}
isChecked={inputProps.checked}
isDisabled={isDisabled}
isHovered={isHovered}
isInvalid={isInvalid}
isSquared={isSquared}
size={size}
>
<StyledRadioContainer className="nextui-radio-container">
<StyledRadioPoint
className="nextui-radio-point"
isFocusVisible={isFocusVisible}
{...focusProps}
>
<VisuallyHidden>
<input
ref={inputRef}
className="nextui-radio-input"
required={isRequired}
{...mergeProps(inputProps, focusProps)}
/>
</VisuallyHidden>
</StyledRadioPoint>
<StyledRadioText
className="nextui-radio-label"
color={labelColor}
isDisabled={isDisabled}
isInvalid={isInvalid}
>
{children}
</StyledRadioText>
</StyledRadioContainer>
<Component {...getBaseProps()}>
<VisuallyHidden>
<input {...getInputProps()} />
</VisuallyHidden>
<span {...getWrapperProps()}>
<span className={slots.point({class: styles?.point})} />
</span>
{children && <span {...getLabelProps()}>{children}</span>}
{description && (
<StyledRadioDescription
className="nextui-radio-description"
isDisabled={isDisabled}
isInvalid={isInvalid}
>
{description}
</StyledRadioDescription>
<span className={slots.description({class: styles?.description})}>{description}</span>
)}
</StyledRadio>
</Component>
);
});
Radio.Group = RadioGroup;
if (__DEV__) {
Radio.displayName = "NextUI.Radio";
}
Radio.toString = () => ".nextui-radio";
export default Radio;

View File

@ -1,67 +1,88 @@
import type {AriaRadioGroupProps} from "@react-types/radio";
import type {Orientation} from "@react-types/shared";
import type {NormalSizes, SimpleColors} from "@nextui-org/shared-utils";
import type {ReactRef} from "@nextui-org/shared-utils";
import type {RadioGroupSlots, SlotsToClasses} from "@nextui-org/theme";
import {useMemo, HTMLAttributes} from "react";
import {radioGroup} from "@nextui-org/theme";
import {useMemo} from "react";
import {RadioGroupState, useRadioGroupState} from "@react-stately/radio";
import {useRadioGroup as useReactAriaRadioGroup} from "@react-aria/radio";
import {HTMLNextUIProps} from "@nextui-org/system";
import {HTMLNextUIProps, PropGetter} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx} from "@nextui-org/shared-utils";
import {mergeProps} from "@react-aria/utils";
interface Props extends HTMLNextUIProps<"div"> {
import {RadioProps} from "./index";
interface Props extends HTMLNextUIProps<"div", AriaRadioGroupProps> {
/**
* The color of the radios.
* @default "default"
* Ref to the DOM node.
*/
color?: SimpleColors;
/**
* The color of the radios's label.
* @default "default"
*/
labelColor?: SimpleColors;
/**
* The size of the radios.
* @default "md"
*/
size?: NormalSizes;
ref?: ReactRef<HTMLDivElement | null>;
/**
* The axis the radio group items should align with.
* @default "vertical"
*/
orientation?: Orientation;
/**
* Classname or List of classes to change the styles of the element.
* if `className` is passed, it will be added to the base slot.
*
* @example
* ```ts
* <RadioGroup styles={{
* base:"base-classes",
* label: "label-classes",
* wrapper: "wrapper-classes", // radios wrapper
* }} >
* // radios
* </RadioGroup>
* ```
*/
styles?: SlotsToClasses<RadioGroupSlots>;
}
export type UseRadioGroupProps = AriaRadioGroupProps & Props;
interface IRadioGroupAria {
/** Props for the radio group wrapper element. */
radioGroupProps: Omit<HTMLAttributes<HTMLElement>, "css">;
/** Props for the radio group's visible label (if any). */
labelProps: Omit<HTMLAttributes<HTMLElement>, "css">;
}
export type UseRadioGroupProps = Props &
Pick<RadioProps, "color" | "size" | "radius" | "isDisabled" | "disableAnimation">;
export type ContextType = {
groupState: RadioGroupState;
isRequired?: UseRadioGroupProps["isRequired"];
color?: UseRadioGroupProps["color"];
labelColor?: UseRadioGroupProps["labelColor"];
size?: UseRadioGroupProps["size"];
validationState?: UseRadioGroupProps["validationState"];
color?: RadioProps["color"];
size?: RadioProps["size"];
radius?: RadioProps["radius"];
isDisabled?: RadioProps["isDisabled"];
disableAnimation?: RadioProps["disableAnimation"];
};
export function useRadioGroup(props: UseRadioGroupProps) {
const {
as,
ref,
styles,
children,
label,
size = "md",
color = "default",
labelColor = "default",
color = "primary",
radius = "full",
isDisabled = false,
disableAnimation = false,
orientation = "vertical",
isRequired,
validationState,
className,
...otherProps
} = props;
const Component = as || "div";
const domRef = useDOMRef(ref);
const otherPropsWithOrientation = useMemo<AriaRadioGroupProps>(() => {
return {
...otherProps,
"aria-label": typeof label === "string" ? label : otherProps["aria-label"],
isRequired,
orientation,
};
@ -69,21 +90,61 @@ export function useRadioGroup(props: UseRadioGroupProps) {
const groupState = useRadioGroupState(otherPropsWithOrientation);
const {labelProps, radioGroupProps: groupProps}: IRadioGroupAria = useReactAriaRadioGroup(
const {labelProps, radioGroupProps: groupProps} = useReactAriaRadioGroup(
otherPropsWithOrientation,
groupState,
);
const context: ContextType = {
size,
color,
labelColor,
groupState,
isRequired,
validationState,
const context: ContextType = useMemo(
() => ({
size,
color,
radius,
groupState,
isRequired,
validationState,
isDisabled,
disableAnimation,
}),
[size, color, radius, groupState, isRequired, validationState, isDisabled, disableAnimation],
);
const slots = useMemo(() => radioGroup(), []);
const baseStyles = clsx(styles?.base, className);
const getGroupProps: PropGetter = () => {
return {
ref: domRef,
className: slots.base({class: baseStyles}),
...mergeProps(groupProps, otherProps),
};
};
return {size, orientation, labelProps, groupProps, context, ...otherProps};
const getLabelProps: PropGetter = () => {
return {
className: slots.label({class: styles?.label}),
...labelProps,
};
};
const getWrapperProps: PropGetter = () => {
return {
className: slots.wrapper({class: styles?.wrapper}),
role: "presentation",
"data-orientation": orientation,
};
};
return {
Component,
children,
label,
context,
getGroupProps,
getLabelProps,
getWrapperProps,
};
}
export type UseRadioGroupReturn = ReturnType<typeof useRadioGroup>;

View File

@ -1,63 +1,69 @@
import type {AriaRadioProps} from "@react-types/radio";
import {RadioVariantProps, RadioSlots, SlotsToClasses, radio} from "@nextui-org/theme";
import {Ref, ReactNode, useCallback} from "react";
import {useMemo, useRef} from "react";
import {useFocusRing} from "@react-aria/focus";
import {useHover} from "@react-aria/interactions";
import {useRadio as useReactAriaRadio} from "@react-aria/radio";
import {HTMLNextUIProps} from "@nextui-org/system";
import {NormalSizes, SimpleColors, __DEV__, warn} from "@nextui-org/shared-utils";
import {HTMLNextUIProps, PropGetter} from "@nextui-org/system";
import {__DEV__, warn, clsx, dataAttr} from "@nextui-org/shared-utils";
import {useDOMRef} from "@nextui-org/dom-utils";
import {mergeProps} from "@react-aria/utils";
import {useRadioGroupContext} from "./radio-group-context";
interface Props extends HTMLNextUIProps<"label"> {
/**
* The content to display as the label.
* Ref to the DOM node.
*/
label?: string;
ref?: Ref<HTMLElement>;
/**
* The color of the radio.
* @default "default"
* The label of the checkbox.
*/
color?: SimpleColors;
/**
* The color of the label.
* @default "default"
*/
labelColor?: SimpleColors;
/**
* The size of the radio.
* @default "md"
*/
size?: NormalSizes;
children?: ReactNode;
/**
* The radio description text.
*/
description?: string;
description?: string | ReactNode;
/**
* Whether the radio is squared.
* @default false
* Classname or List of classes to change the styles of the element.
* if `className` is passed, it will be added to the base slot.
*
* @example
* ```ts
* <Radio styles={{
* base:"base-classes",
* wrapper: "wrapper-classes",
* point: "control-classes", // inner circle
* label: "label-classes",
* description: "description-classes",
* }} />
* ```
*/
isSquared?: boolean;
/**
* Whether the radio has animations.
* @default false
*/
disableAnimation?: boolean;
styles?: SlotsToClasses<RadioSlots>;
}
export type UseRadioProps = Props & AriaRadioProps;
export type UseRadioProps = Props &
Omit<AriaRadioProps, keyof RadioVariantProps> &
Omit<RadioVariantProps, "isFocusVisible">;
export function useRadio(props: UseRadioProps) {
const groupContext = useRadioGroupContext();
const {
as,
ref,
styles,
children,
description,
size = groupContext?.size ?? "md",
color = groupContext?.color ?? "default",
labelColor = groupContext?.labelColor ?? "default",
color = groupContext?.color ?? "primary",
radius = groupContext?.radius ?? "full",
isDisabled: isDisabledProp = groupContext?.isDisabled ?? false,
disableAnimation = groupContext?.disableAnimation ?? false,
autoFocus = false,
isSquared = false,
isDisabled: isDisabledProp = false,
disableAnimation = false,
className,
...otherProps
} = props;
@ -70,55 +76,121 @@ export function useRadio(props: UseRadioProps) {
}
}
const Component = as || "label";
const domRef = useDOMRef(ref);
const inputRef = useRef<HTMLInputElement>(null);
const {inputProps} = useReactAriaRadio(
{
...otherProps,
...groupContext,
isDisabled: isDisabledProp,
},
groupContext.groupState,
inputRef,
);
const isDisabled = useMemo(() => inputProps.disabled ?? false, [inputProps.disabled]);
const isDisabled = useMemo(() => !!isDisabledProp, [isDisabledProp]);
const isRequired = useMemo(() => groupContext.isRequired ?? false, [groupContext.isRequired]);
const isInvalid = useMemo(
() => groupContext.validationState === "invalid",
[groupContext.validationState],
);
const ariaRadioProps = useMemo(() => {
const arialabel =
otherProps["aria-label"] || typeof children === "string" ? (children as string) : undefined;
const ariaDescribedBy =
otherProps["aria-describedby"] || typeof description === "string"
? (description as string)
: undefined;
return {
isDisabled,
isRequired,
"aria-label": arialabel,
"aria-labelledby": otherProps["aria-labelledby"] || arialabel,
"aria-describedby": otherProps["aria-describedby"] || ariaDescribedBy,
};
}, [isDisabled, isRequired]);
const {inputProps} = useReactAriaRadio(
{
...otherProps,
...groupContext,
...ariaRadioProps,
},
groupContext.groupState,
inputRef,
);
const {hoverProps, isHovered} = useHover({isDisabled});
const {focusProps, isFocusVisible} = useFocusRing({
const {focusProps, isFocused, isFocusVisible} = useFocusRing({
autoFocus,
});
const state = useMemo(() => {
if (isHovered) return "hovered";
if (isDisabled) return "disabled";
const slots = useMemo(
() =>
radio({
color,
size,
radius,
isDisabled,
isFocusVisible,
disableAnimation,
}),
[color, size, radius, isDisabled, isFocusVisible, disableAnimation],
);
return inputProps.checked ? "checked" : "uncheked";
}, [isDisabled, inputProps.checked, isHovered]);
const baseStyles = clsx(styles?.base, className);
const getBaseProps: PropGetter = () => {
return {
ref: domRef,
className: slots.base({class: baseStyles}),
"data-disabled": dataAttr(isDisabled),
"data-checked": dataAttr(inputProps.checked),
"data-invalid": dataAttr(isInvalid),
...mergeProps(hoverProps, otherProps),
};
};
const getWrapperProps: PropGetter = () => {
return {
"data-active": dataAttr(inputProps.checked),
"data-hover": dataAttr(isHovered),
"data-checked": dataAttr(inputProps.checked),
"data-focus": dataAttr(isFocused),
"data-focus-visible": dataAttr(isFocused && isFocusVisible),
"data-disabled": dataAttr(isDisabled),
"data-invalid": dataAttr(isInvalid),
"data-readonly": dataAttr(inputProps.readOnly),
"aria-required": dataAttr(isRequired),
"aria-hidden": true,
className: clsx(slots.wrapper({class: styles?.wrapper})),
};
};
const getInputProps: PropGetter = () => {
return {
ref: inputRef,
"data-readonly": dataAttr(inputProps.readOnly),
...mergeProps(inputProps, focusProps),
};
};
const getLabelProps: PropGetter = useCallback(
() => ({
"data-disabled": dataAttr(isDisabled),
"data-checked": dataAttr(inputProps.checked),
"data-invalid": dataAttr(isInvalid),
className: slots.label({class: styles?.label}),
}),
[slots, isDisabled, inputProps.checked, isInvalid],
);
return {
state,
size,
color,
labelColor,
inputRef,
isDisabled,
isRequired,
isInvalid,
isHovered,
isSquared,
isFocusVisible,
disableAnimation,
hoverProps,
focusProps,
inputProps,
...otherProps,
Component,
children,
slots,
styles,
description,
getBaseProps,
getWrapperProps,
getInputProps,
getLabelProps,
};
}

View File

@ -1,247 +1,303 @@
import React from "react";
import {Meta} from "@storybook/react";
import {Button} from "@nextui-org/button";
import {Spacer} from "@nextui-org/spacer";
import {ComponentStory, ComponentMeta} from "@storybook/react";
import {radio} from "@nextui-org/theme";
import {Radio} from "../src";
import {RadioGroup, Radio, RadioGroupProps} from "../src";
export default {
title: "Inputs/Radio",
component: Radio,
title: "Inputs/RadioGroup",
component: RadioGroup,
onChange: {action: "changed"},
} as Meta;
argTypes: {
color: {
control: {
type: "select",
options: ["neutral", "primary", "secondary", "success", "warning", "danger"],
},
},
radius: {
control: {
type: "select",
options: ["none", "base", "sm", "md", "lg", "xl", "full"],
},
},
size: {
control: {
type: "select",
options: ["xs", "sm", "md", "lg", "xl"],
},
},
isDisabled: {
control: {
type: "boolean",
},
},
},
} as ComponentMeta<typeof RadioGroup>;
export const Default = () => (
<Radio.Group label="Options">
const defaultProps = {
...radio.defaultVariants,
label: "Options",
};
const Template: ComponentStory<typeof RadioGroup> = (args: RadioGroupProps) => (
<RadioGroup {...args}>
<Radio value="A">Option A</Radio>
<Radio value="B">Option B</Radio>
<Radio value="C">Option C</Radio>
<Radio value="D">Option D</Radio>
</Radio.Group>
</RadioGroup>
);
const handleSubmit = (e: any) => {
e.preventDefault();
alert("Submitted!");
export const Default = Template.bind({});
Default.args = {
...defaultProps,
};
export const Required = () => (
<form onSubmit={handleSubmit}>
<Radio.Group isRequired label="Options">
<Radio value="A">Option A</Radio>
<Radio value="B">Option B</Radio>
<Radio value="C">Option C</Radio>
<Radio value="D">Option D</Radio>
</Radio.Group>
<Spacer y={1} />
<Button type="submit">Submit</Button>
</form>
);
// import React from "react";
// import {Meta} from "@storybook/react";
// import {Button} from "@nextui-org/button";
// import {Spacer} from "@nextui-org/spacer";
export const Disabled = () => (
<Radio.Group isDisabled defaultValue="A" label="Options">
<Radio description="Description for Option A" value="A">
Option A
</Radio>
<Radio value="B">Option B</Radio>
<Radio value="C">Option C</Radio>
<Radio value="D">Option D</Radio>
</Radio.Group>
);
// import {Radio} from "../src";
export const Sizes = () => {
return (
<div style={{display: "flex", flexDirection: "row", gap: 200}}>
<Radio.Group defaultValue="md" label="Sizes">
<Radio size="xs" value="xs">
mini
</Radio>
<Radio size="sm" value="sm">
small
</Radio>
<Radio size="md" value="md">
medium
</Radio>
<Radio size="lg" value="lg">
large
</Radio>
<Radio size="xl" value="xl">
xlarge
</Radio>
</Radio.Group>
<Radio.Group defaultValue="md" label="Sizes">
<Radio description="Description for Option mini" size="xs" value="xs">
mini
</Radio>
<Radio description="Description for Option small" size="sm" value="sm">
small
</Radio>
<Radio description="Description for Option medium" size="md" value="md">
medium
</Radio>
<Radio description="Description for Option large" size="lg" value="lg">
large
</Radio>
<Radio description="Description for Option xlarge" size="xl" value="xl">
xlarge
</Radio>
</Radio.Group>
</div>
);
};
// export default {
// title: "Inputs/Radio",
// component: Radio,
// onChange: {action: "changed"},
// } as Meta;
export const Colors = () => {
return (
<Radio.Group defaultValue="primary" label="Colors">
<Radio color="primary" value="primary">
primary
</Radio>
<Radio color="secondary" value="secondary">
secondary
</Radio>
<Radio color="success" value="success">
success
</Radio>
<Radio color="warning" value="warning">
warning
</Radio>
<Radio color="error" value="error">
error
</Radio>
</Radio.Group>
);
};
// export const Default = () => (
// <Radio.Group label="Options">
// <Radio value="A">Option A</Radio>
// <Radio value="B">Option B</Radio>
// <Radio value="C">Option C</Radio>
// <Radio value="D">Option D</Radio>
// </Radio.Group>
// );
export const LabelColors = () => {
return (
<Radio.Group defaultValue="primary" label="Label colors">
<Radio color="primary" labelColor="primary" value="primary">
primary
</Radio>
<Radio color="secondary" labelColor="secondary" value="secondary">
secondary
</Radio>
<Radio color="success" labelColor="success" value="success">
success
</Radio>
<Radio color="warning" labelColor="warning" value="warning">
warning
</Radio>
<Radio color="error" labelColor="error" value="error">
error
</Radio>
</Radio.Group>
);
};
// const handleSubmit = (e: any) => {
// e.preventDefault();
// alert("Submitted!");
// };
export const Squared = () => (
<Radio.Group defaultValue="A" label="Options">
<Radio isSquared value="A">
Option A
</Radio>
<Radio isSquared value="B">
Option B
</Radio>
<Radio isSquared value="C">
Option C
</Radio>
<Radio isSquared value="D">
Option D
</Radio>
</Radio.Group>
);
// export const Required = () => (
// <form onSubmit={handleSubmit}>
// <Radio.Group isRequired label="Options">
// <Radio value="A">Option A</Radio>
// <Radio value="B">Option B</Radio>
// <Radio value="C">Option C</Radio>
// <Radio value="D">Option D</Radio>
// </Radio.Group>
// <Spacer y={1} />
// <Button type="submit">Submit</Button>
// </form>
// );
export const Description = () => (
<Radio.Group defaultValue="A" label="Options">
<Radio description="Description for Option A" value="A">
Option A
</Radio>
<Radio description="Description for Option B" value="B">
Option B
</Radio>
<Radio description="Description for Option C" value="C">
Option C
</Radio>
<Radio description="Description for Option D" value="D">
Option D
</Radio>
</Radio.Group>
);
// export const Disabled = () => (
// <Radio.Group isDisabled defaultValue="A" label="Options">
// <Radio description="Description for Option A" value="A">
// Option A
// </Radio>
// <Radio value="B">Option B</Radio>
// <Radio value="C">Option C</Radio>
// <Radio value="D">Option D</Radio>
// </Radio.Group>
// );
export const Invalid = () => (
<Radio.Group defaultValue="A" label="Options" validationState="invalid">
<Radio description="Description for Option A" value="A">
Option A
</Radio>
<Radio description="Description for Option B" value="B">
Option B
</Radio>
<Radio description="Description for Option C" value="C">
Option C
</Radio>
<Radio description="Description for Option D" value="D">
Option D
</Radio>
</Radio.Group>
);
// export const Sizes = () => {
// return (
// <div style={{display: "flex", flexDirection: "row", gap: 200}}>
// <Radio.Group defaultValue="md" label="Sizes">
// <Radio size="xs" value="xs">
// mini
// </Radio>
// <Radio size="sm" value="sm">
// small
// </Radio>
// <Radio size="md" value="md">
// medium
// </Radio>
// <Radio size="lg" value="lg">
// large
// </Radio>
// <Radio size="xl" value="xl">
// xlarge
// </Radio>
// </Radio.Group>
// <Radio.Group defaultValue="md" label="Sizes">
// <Radio description="Description for Option mini" size="xs" value="xs">
// mini
// </Radio>
// <Radio description="Description for Option small" size="sm" value="sm">
// small
// </Radio>
// <Radio description="Description for Option medium" size="md" value="md">
// medium
// </Radio>
// <Radio description="Description for Option large" size="lg" value="lg">
// large
// </Radio>
// <Radio description="Description for Option xlarge" size="xl" value="xl">
// xlarge
// </Radio>
// </Radio.Group>
// </div>
// );
// };
export const Row = () => (
<div style={{display: "flex", flexDirection: "column", gap: 100}}>
<Radio.Group defaultValue="A" label="Options" orientation="horizontal">
<Radio value="A">Option A</Radio>
<Radio value="B">Option B</Radio>
<Radio value="C">Option C</Radio>
<Radio value="D">Option D</Radio>
</Radio.Group>
<Radio.Group defaultValue="A" label="Options" orientation="horizontal">
<Radio description="Description for Option A" value="A">
Option A
</Radio>
<Radio description="Description for Option B" value="B">
Option B
</Radio>
<Radio description="Description for Option C" value="C">
Option C
</Radio>
<Radio description="Description for Option D" value="D">
Option D
</Radio>
</Radio.Group>
</div>
);
// export const Colors = () => {
// return (
// <Radio.Group defaultValue="primary" label="Colors">
// <Radio color="primary" value="primary">
// primary
// </Radio>
// <Radio color="secondary" value="secondary">
// secondary
// </Radio>
// <Radio color="success" value="success">
// success
// </Radio>
// <Radio color="warning" value="warning">
// warning
// </Radio>
// <Radio color="error" value="error">
// error
// </Radio>
// </Radio.Group>
// );
// };
export const Controlled = () => {
const [checked, setChecked] = React.useState<string>("london");
// export const LabelColors = () => {
// return (
// <Radio.Group defaultValue="primary" label="Label colors">
// <Radio color="primary" labelColor="primary" value="primary">
// primary
// </Radio>
// <Radio color="secondary" labelColor="secondary" value="secondary">
// secondary
// </Radio>
// <Radio color="success" labelColor="success" value="success">
// success
// </Radio>
// <Radio color="warning" labelColor="warning" value="warning">
// warning
// </Radio>
// <Radio color="error" labelColor="error" value="error">
// error
// </Radio>
// </Radio.Group>
// );
// };
React.useEffect(() => {
console.log("checked:", checked);
}, [checked]);
// export const Squared = () => (
// <Radio.Group defaultValue="A" label="Options">
// <Radio isSquared value="A">
// Option A
// </Radio>
// <Radio isSquared value="B">
// Option B
// </Radio>
// <Radio isSquared value="C">
// Option C
// </Radio>
// <Radio isSquared value="D">
// Option D
// </Radio>
// </Radio.Group>
// );
return (
<Radio.Group label="Check cities" value={checked} onChange={(value) => setChecked(value)}>
<Radio value="buenos-aires">Buenos Aires</Radio>
<Radio value="sydney">Sydney</Radio>
<Radio value="london">London</Radio>
<Radio value="tokyo">Tokyo</Radio>
</Radio.Group>
);
};
// export const Description = () => (
// <Radio.Group defaultValue="A" label="Options">
// <Radio description="Description for Option A" value="A">
// Option A
// </Radio>
// <Radio description="Description for Option B" value="B">
// Option B
// </Radio>
// <Radio description="Description for Option C" value="C">
// Option C
// </Radio>
// <Radio description="Description for Option D" value="D">
// Option D
// </Radio>
// </Radio.Group>
// );
export const DisableAnimation = () => {
return (
<Radio.Group defaultValue="A" label="Options">
<Radio disableAnimation value="A">
Option A
</Radio>
<Radio disableAnimation value="B">
Option B
</Radio>
<Radio disableAnimation value="C">
Option C
</Radio>
<Radio disableAnimation value="D">
Option D
</Radio>
</Radio.Group>
);
};
// export const Invalid = () => (
// <Radio.Group defaultValue="A" label="Options" validationState="invalid">
// <Radio description="Description for Option A" value="A">
// Option A
// </Radio>
// <Radio description="Description for Option B" value="B">
// Option B
// </Radio>
// <Radio description="Description for Option C" value="C">
// Option C
// </Radio>
// <Radio description="Description for Option D" value="D">
// Option D
// </Radio>
// </Radio.Group>
// );
// export const Row = () => (
// <div style={{display: "flex", flexDirection: "column", gap: 100}}>
// <Radio.Group defaultValue="A" label="Options" orientation="horizontal">
// <Radio value="A">Option A</Radio>
// <Radio value="B">Option B</Radio>
// <Radio value="C">Option C</Radio>
// <Radio value="D">Option D</Radio>
// </Radio.Group>
// <Radio.Group defaultValue="A" label="Options" orientation="horizontal">
// <Radio description="Description for Option A" value="A">
// Option A
// </Radio>
// <Radio description="Description for Option B" value="B">
// Option B
// </Radio>
// <Radio description="Description for Option C" value="C">
// Option C
// </Radio>
// <Radio description="Description for Option D" value="D">
// Option D
// </Radio>
// </Radio.Group>
// </div>
// );
// export const Controlled = () => {
// const [checked, setChecked] = React.useState<string>("london");
// React.useEffect(() => {
// console.log("checked:", checked);
// }, [checked]);
// return (
// <Radio.Group label="Check cities" value={checked} onChange={(value) => setChecked(value)}>
// <Radio value="buenos-aires">Buenos Aires</Radio>
// <Radio value="sydney">Sydney</Radio>
// <Radio value="london">London</Radio>
// <Radio value="tokyo">Tokyo</Radio>
// </Radio.Group>
// );
// };
// export const DisableAnimation = () => {
// return (
// <Radio.Group defaultValue="A" label="Options">
// <Radio disableAnimation value="A">
// Option A
// </Radio>
// <Radio disableAnimation value="B">
// Option B
// </Radio>
// <Radio disableAnimation value="C">
// Option C
// </Radio>
// <Radio disableAnimation value="D">
// Option D
// </Radio>
// </Radio.Group>
// );
// };

View File

@ -2,11 +2,7 @@ import type {As, RightJoinProps, PropsOf, ComponentWithAs} from "./types";
import {forwardRef as baseForwardRef} from "react";
export function forwardRef<
Props extends object,
Component extends As,
CompoundComponents extends object = {},
>(
export function forwardRef<Props extends object, Component extends As>(
component: React.ForwardRefRenderFunction<
any,
RightJoinProps<PropsOf<Component>, Props> & {
@ -14,8 +10,7 @@ export function forwardRef<
}
>,
) {
return baseForwardRef(component) as unknown as ComponentWithAs<Component, Props> &
CompoundComponents;
return baseForwardRef(component) as unknown as ComponentWithAs<Component, Props>;
}
export const toIterator = (obj: any) => {

View File

@ -3,7 +3,7 @@ import {tv} from "tailwind-variants";
/**
* CheckboxGroup wrapper **Tailwind Variants** component
*
* const {base, label, wrapper,} = checkboxGroup({...})
* const {base, label, wrapper} = checkboxGroup({...})
*
* @example
* <div className={base())}>

View File

@ -13,3 +13,5 @@ export * from "./chip";
export * from "./badge";
export * from "./checkbox";
export * from "./checkbox-group";
export * from "./radio";
export * from "./radio-group";

View File

@ -0,0 +1,26 @@
import {tv} from "tailwind-variants";
/**
* RadioGroup wrapper **Tailwind Variants** component
*
* const {base, label, wrapper} = radioGroup({...})
*
* @example
* <div className={base())}>
* <label className={label()}>Label</label>
* <div className={wrapper()} data-orientation="vertical/horizontal">
* // radios
* </div>
* </div>
*/
const radioGroup = tv({
slots: {
base: "relative flex flex-col gap-2",
label: "relative text-neutral-500",
wrapper: "flex flex-col flex-wrap gap-2 data-[orientation=horizontal]:flex-row ",
},
});
export type RadioGroupSlots = keyof ReturnType<typeof radioGroup>;
export {radioGroup};

View File

@ -0,0 +1,80 @@
import {tv, type VariantProps} from "tailwind-variants";
import {ringClasses} from "../utils";
/**
* Radio wrapper **Tailwind Variants** component
*
* const {base, wrapper, point, label, description} = radio({...})
*
* @example
* <label className={base())}>
* // input
* <span className={wrapper()} aria-hidden="true" data-checked={checked}>
* <span className={point()}/>
* </span>
* <span className={label()}>Label</span>
* <span className={description()}>Description</span>
* </label>
*/
const radio = tv({
slots: {
base: "relative max-w-fit inline-flex items-center justify-start cursor-pointer",
wrapper: "",
point: "",
label: "relative ml-1 text-foreground select-none",
description: "relative ml-1 text-neutral-500 select-none",
},
variants: {
color: {
neutral: {},
primary: {},
secondary: {},
success: {},
warning: {},
danger: {},
},
size: {
xs: {},
sm: {},
md: {},
lg: {},
xl: {},
},
radius: {
none: {},
base: {},
sm: {},
md: {},
lg: {},
xl: {},
full: {},
},
isDisabled: {
true: {
base: "opacity-50 pointer-events-none",
},
},
isFocusVisible: {
true: {
wrapper: [...ringClasses],
},
},
disableAnimation: {
true: {},
false: {},
},
},
defaultVariants: {
color: "primary",
size: "md",
radius: "md",
isDisabled: false,
disableAnimation: false,
},
});
export type RadioVariantProps = VariantProps<typeof radio>;
export type RadioSlots = keyof ReturnType<typeof radio>;
export {radio};

View File

@ -12,6 +12,7 @@ module.exports = {
"../../components/chip/stories/*.stories.@(js|jsx|ts|tsx)",
"../../components/badge/stories/*.stories.@(js|jsx|ts|tsx)",
"../../components/checkbox/stories/*.stories.@(js|jsx|ts|tsx)",
"../../components/radio/stories/*.stories.@(js|jsx|ts|tsx)",
],
staticDirs: ["../public"],
addons: [

4
pnpm-lock.yaml generated
View File

@ -621,9 +621,9 @@ importers:
specifiers:
'@nextui-org/button': workspace:*
'@nextui-org/dom-utils': workspace:*
'@nextui-org/shared-css': workspace:*
'@nextui-org/shared-utils': workspace:*
'@nextui-org/system': workspace:*
'@nextui-org/theme': workspace:*
'@react-aria/focus': ^3.11.0
'@react-aria/interactions': ^3.14.0
'@react-aria/radio': ^3.5.0
@ -636,9 +636,9 @@ importers:
react: ^18.2.0
dependencies:
'@nextui-org/dom-utils': link:../../utilities/dom-utils
'@nextui-org/shared-css': link:../../utilities/shared-css
'@nextui-org/shared-utils': link:../../utilities/shared-utils
'@nextui-org/system': link:../../core/system
'@nextui-org/theme': link:../../core/theme
'@react-aria/focus': 3.11.0_react@18.2.0
'@react-aria/interactions': 3.14.0_react@18.2.0
'@react-aria/radio': 3.5.0_react@18.2.0