Skip to content

Commit

Permalink
feat(radio): added radio component (#45)
Browse files Browse the repository at this point in the history
* feat(radio): added initial radio styles

* feat(radio): radio with tailwind forms (#46)

* chore(radio): radio with tailwind forms

* chore(radio): added disabled state styles

* chore: purge fix

* chore: added controllable state

* chore: addComponent for radio styles

* chore(radio): added RadioLabel

* chore(radio): radio label improvements

* chore: remove css file

* chore: changed spinner types to getThemeValue

* chore: added Box in radio label

* feat(radio): added size prop

* fix(radio): fix radio state context

* chore: radio types cleanup

* feat(radio): added radio v2 (#49)

* feat(radio): migrate radio to use conditional rendering

* refactor(radio): added size support and refactored all types

* chore: radio controlled state

* chore: review updates

* fix(radio): fixed radio controlled state

* fix(radio): fixed radio controlled state

* refactor: rearrange imports

* refactor(radio): added radio icons with css

* Revert "refactor(radio): added radio icons with css"

This reverts commit 5572582.

* refactor(radio): minor updates

* feat(radio): added icon props in radio

* chore: refactor & remove old code
  • Loading branch information
anuraghazra authored Feb 1, 2021
1 parent 67c9498 commit a341146
Show file tree
Hide file tree
Showing 14 changed files with 887 additions and 390 deletions.
5 changes: 4 additions & 1 deletion .storybook/storybookUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ export const storyTemplate = <ComponentProps,>(

export const createUnionControl = (keys: any) => {
return {
control: { type: "inline-radio", options: Object.keys(keys) },
control: {
type: "inline-radio",
options: Array.isArray(keys) ? keys : Object.keys(keys),
},
};
};

Expand Down
41 changes: 41 additions & 0 deletions src/icons/RadioIcons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as React from "react";
import { createIcon } from "../icon";

export const RadioCheckedIcon = createIcon({
displayName: "RadioChecked",
viewBox: "0 0 16 16",
path: (
<>
<circle cx="8" cy="8" r="8" fill="currentColor" />
<circle cx="8" cy="8" r="3" fill="white" />
</>
),
});

export const RadioUncheckedIcon = createIcon({
displayName: "RadioUnchecked",
viewBox: "0 0 16 16",
path: (
<>
<circle
cx="8"
cy="8"
r="7"
fill="white"
stroke="currentColor"
strokeWidth="1.5"
/>
</>
),
});

export const RadioDisabledIcon = createIcon({
displayName: "RadioDisabled",
viewBox: "0 0 16 16",
path: (
<>
<circle cx="8" cy="8" r="8" fill="#E4E4E7" />
<circle cx="8" cy="8" r="3" fill="currentColor" />
</>
),
});
55 changes: 55 additions & 0 deletions src/radio/Radio.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from "react";
import {
RadioHTMLProps,
Radio as ReakitRadio,
RadioProps as ReakitRadioProps,
} from "reakit";
import { cx } from "@renderlesskit/react";

import { useTheme } from "../theme";
import { RadioIcon } from "./RadioIcon";
import { useRadioContext } from "./RadioGroup";
import { forwardRefWithAs } from "../utils/types";

export type RadioCommonProps = Partial<
Pick<ReakitRadioProps, "value" | "disabled">
> & {
size?: keyof Renderlesskit.GetThemeValue<"radio", "icon">["size"];
};

export const RadioInput: React.FC<RadioHTMLProps & RadioCommonProps> = ({
className,
...rest
}) => {
const theme = useTheme();
const { radioState } = useRadioContext();

const radioStyles = cx(theme.radio.input, className);

return <ReakitRadio className={radioStyles} {...radioState} {...rest} />;
};

export const Radio = forwardRefWithAs<
RadioHTMLProps &
RadioCommonProps & {
checkedIcon?: React.ReactNode;
uncheckedIcon?: React.ReactNode;
disabledIcon?: React.ReactNode;
},
HTMLInputElement,
"input"
>((props, ref) => {
const { checkedIcon, uncheckedIcon, disabledIcon } = props;
return (
<>
<RadioInput ref={ref} {...props} />
<RadioIcon
value={props.value}
disabled={props.disabled}
checkedIcon={checkedIcon}
uncheckedIcon={uncheckedIcon}
disabledIcon={disabledIcon}
/>
</>
);
});
40 changes: 40 additions & 0 deletions src/radio/RadioGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from "react";
import { RadioInitialState, RadioStateReturn } from "reakit";

import { createContext } from "../utils";
import { RadioCommonProps } from "./Radio";
import { useRadioState } from "./useRadioState";

type RadioContextType = {
radioState?: RadioStateReturn;
radioSize?: RadioCommonProps["size"];
};

const [RadioProvider, useRadioContext] = createContext<RadioContextType>({
errorMessage: "Radio must be used within RadioProvider",
name: "RadioContext",
strict: true,
});

export { RadioProvider, useRadioContext };

export type RadioGroupProps = RadioInitialState &
Pick<RadioCommonProps, "size"> & {
onStateChange?: (state: RadioCommonProps["value"]) => void;
state?: RadioCommonProps["value"];
defaultState?: RadioCommonProps["value"];
};

export const RadioGroup: React.FC<RadioGroupProps> = ({
children,
size = "sm",
...props
}) => {
const radioState = useRadioState(props);

return (
<RadioProvider value={{ radioState: radioState, radioSize: size }}>
{children}
</RadioProvider>
);
};
70 changes: 70 additions & 0 deletions src/radio/RadioIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from "react";
import { cx } from "@renderlesskit/react";
import { Box, BoxProps, RadioState } from "reakit";

import {
RadioCheckedIcon,
RadioDisabledIcon,
RadioUncheckedIcon,
} from "../icons/RadioIcons";
import { useTheme } from "..";
import { RadioCommonProps } from "./Radio";
import { useRadioContext } from "./RadioGroup";
import { forwardRefWithAs } from "../utils/types";

export type RadioIconProps = BoxProps &
RadioState &
RadioCommonProps & {
checkedIcon?: React.ReactNode;
uncheckedIcon?: React.ReactNode;
disabledIcon?: React.ReactNode;
};

export const RadioIcon = forwardRefWithAs<
Partial<RadioIconProps>,
HTMLDivElement,
"div"
>((props, ref) => {
const { radioState, radioSize } = useRadioContext();
const { value, size, disabled, ...mainProps } = props;
const { className, children, ...rest } = mainProps;

const _size = size || radioSize || "sm";
const stateProp = radioState?.state === value;

const theme = useTheme();
const radioIconStyles = cx(
theme.radio.icon.base,
theme.radio.icon.size[_size],
disabled
? theme.radio.icon.disabled
: stateProp
? theme.radio.icon.checked
: theme.radio.icon.unchecked,
className,
);

const iconMap = {
checked: props.checkedIcon || <RadioCheckedIcon />,
unchecked: props.uncheckedIcon || <RadioUncheckedIcon />,
disabled: props.disabledIcon || <RadioDisabledIcon />,
};

return (
<Box
ref={ref}
role="img"
aria-hidden="true"
className={radioIconStyles}
{...rest}
>
{children
? children
: disabled
? iconMap.disabled
: stateProp
? iconMap.checked
: iconMap.unchecked}
</Box>
);
});
33 changes: 33 additions & 0 deletions src/radio/RadioLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from "react";
import { BoxProps } from "reakit";
import { cx } from "@renderlesskit/react";

import { Box } from "../box";
import { useTheme } from "..";
import { RadioCommonProps } from "./Radio";
import { useRadioContext } from "./RadioGroup";
import { forwardRefWithAs } from "../utils/types";

export type RadioLabelProps = BoxProps & Omit<RadioCommonProps, "value">;

export const RadioLabel = forwardRefWithAs<
RadioLabelProps,
HTMLLabelElement,
"label"
>((props, ref) => {
const { size, disabled = false, ...mainProps } = props;
const { className, ...rest } = mainProps;
const theme = useTheme();
const { radioSize } = useRadioContext();
const _size = size || radioSize || "sm";

const radioStyles = cx(
theme.radio.base,
theme.radio.label.base,
theme.radio.label.size[_size],
disabled ? theme.radio.disabled : "",
className,
);

return <Box as="label" ref={ref} className={radioStyles} {...rest} />;
});
5 changes: 5 additions & 0 deletions src/radio/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from "./Radio";
export * from "./RadioIcon";
export * from "./RadioLabel";
export * from "./RadioGroup";
export * from "./useRadioState";
129 changes: 129 additions & 0 deletions src/radio/stories/Radio.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import React from "react";
import { Meta } from "@storybook/react";

import { Button } from "../../button";
import {
storyTemplate,
createUnionControl,
} from "../../../.storybook/storybookUtils";
import {
RadioIcon,
RadioInput,
RadioGroup,
RadioLabel,
Radio,
RadioGroupProps,
} from "../index";
import { WheelIcon } from "../../icons";
import { InfoCircle } from "../../icon/stories/Icon.stories";

export default {
title: "Radio",
component: Radio,
argTypes: {
size: createUnionControl(["xs", "sm", "lg"]),
},
} as Meta;

const base = storyTemplate<RadioGroupProps>(
args => {
return (
<RadioGroup {...args}>
<div className="flex gap-3">
<RadioLabel>
<Radio value="1" />
label 1
</RadioLabel>

<RadioLabel>
<Radio value="2" />
label 2
</RadioLabel>
</div>
</RadioGroup>
);
},
{
defaultState: "2",
size: "sm",
},
);

export const Default = base({});

export const States = () => {
return (
<RadioGroup defaultState={"2"}>
<div className="flex flex-col gap-2">
<RadioLabel className="hover:bg-gray-100 p-2 rounded-md">
<Radio value="1" />
Unchecked
</RadioLabel>
<RadioLabel className="hover:bg-gray-100 p-2 rounded-md">
<Radio value="2" />
Checked
</RadioLabel>
<RadioLabel className="hover:bg-gray-100 p-2 rounded-md">
<Radio value="3" disabled />
Disabled
</RadioLabel>
</div>
</RadioGroup>
);
};

export const Controlled = () => {
const [state, setState] = React.useState("1");
return (
<>
<RadioGroup state={state} onStateChange={e => setState(e as string)}>
<div className="flex flex-col gap-2">
<RadioLabel className="hover:bg-gray-100 p-2 rounded-md">
<Radio value="1" />
Unchecked
</RadioLabel>
<RadioLabel className="hover:bg-gray-100 p-2 rounded-md">
<Radio value="2" />
Checked
</RadioLabel>
<RadioLabel className="hover:bg-gray-100 p-2 rounded-md">
<Radio value="3" />
Disabled
</RadioLabel>
</div>
</RadioGroup>
<Button onClick={() => setState("2")}>change</Button>
</>
);
};

export const CustomIcon = () => {
const [state, setState] = React.useState("1");
return (
<>
<RadioGroup state={state} onStateChange={e => setState(e as string)}>
<div className="flex flex-col gap-2">
<RadioLabel className="hover:bg-gray-100 p-2 rounded-md">
<RadioInput value="1" />
<RadioIcon
value="1"
checkedIcon={<WheelIcon />}
uncheckedIcon={<InfoCircle />}
/>
Two
</RadioLabel>
<RadioLabel className="hover:bg-gray-100 p-2 rounded-md">
<RadioInput value="2" />
<RadioIcon
value="2"
checkedIcon={<WheelIcon />}
uncheckedIcon={<InfoCircle />}
/>
Two
</RadioLabel>
</div>
</RadioGroup>
<Button onClick={() => setState("2")}>change</Button>
</>
);
};
Loading

1 comment on commit a341146

@vercel
Copy link

@vercel vercel bot commented on a341146 Feb 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.