diff --git a/docs-templates/checkbox.md b/docs-templates/checkbox.md
index e45340cf5..2ae942c03 100644
--- a/docs-templates/checkbox.md
+++ b/docs-templates/checkbox.md
@@ -25,7 +25,7 @@ js: src/checkbox/stories/templates/CheckboxBasicJsx.ts
- When not checked, Checkbox has `aria-checked` set to `false`.
- When partially checked, Checkbox has `aria-checked` set to `mixed`.
- When Checkbox is not rendered as a native input checkbox, Checkbox will add
- `role="checkbox`
+ `role="checkbox"`
diff --git a/docs-templates/radio.md b/docs-templates/radio.md
new file mode 100644
index 000000000..a9ab6ad6f
--- /dev/null
+++ b/docs-templates/radio.md
@@ -0,0 +1,34 @@
+# Radio
+
+`Radio` component follows the
+[WAI-ARIA Radio Pattern](https://w3c.github.io/aria-practices/#radiobutton) for
+it's
+[accessibility properties](https://w3c.github.io/aria-practices/#wai-aria-roles-states-and-properties-16).
+By default, it renders the native ``.
+
+
+
+## Usage
+
+
+
+
+
+## Accessibility Requirement
+
+- Radio has role `radio`.
+- Radio has aria-checked set to true when it's checked. Otherwise, aria-checked
+ is set to false.
+- Radio extends the accessibility features of CompositeItem, which means it uses
+ the roving tabindex method to manage focus.
+- When Radio is not rendered as a native input checkbox, Radio will add
+ `role="radio"`
+- RadioGroup has role `radiogroup`.
+- RadioGroup must has `aria-label` or `aria-labelledby` to describe the group.
+
+
+
+
diff --git a/docs/checkbox.md b/docs/checkbox.md
index 277a233bb..fe1c82004 100644
--- a/docs/checkbox.md
+++ b/docs/checkbox.md
@@ -51,7 +51,7 @@ export default Checkbox;
- When not checked, Checkbox has `aria-checked` set to `false`.
- When partially checked, Checkbox has `aria-checked` set to `mixed`.
- When Checkbox is not rendered as a native input checkbox, Checkbox will add
- `role="checkbox`
+ `role="checkbox"`
## Composition
diff --git a/docs/radio.md b/docs/radio.md
new file mode 100644
index 000000000..0e7e9575d
--- /dev/null
+++ b/docs/radio.md
@@ -0,0 +1,152 @@
+# Radio
+
+`Radio` component follows the
+[WAI-ARIA Radio Pattern](https://w3c.github.io/aria-practices/#radiobutton) for
+it's
+[accessibility properties](https://w3c.github.io/aria-practices/#wai-aria-roles-states-and-properties-16).
+By default, it renders the native ``.
+
+## Table of Contents
+
+- [Usage](#usage)
+- [Accessibility Requirement](#accessibility-requirement)
+- [Composition](#composition)
+- [Props](#props)
+ - [`useRadioState`](#useradiostate)
+ - [`Radio`](#radio)
+ - [`RadioGroup`](#radiogroup)
+
+## Usage
+
+```js
+import * as React from "react";
+
+import {
+ useRadioState,
+ Radio as RenderlesskitRadio,
+ USE_RADIO_STATE_KEYS,
+ splitStateProps,
+ RadioGroup,
+} from "@renderlesskit/react";
+
+export const Radio = props => {
+ const [stateProps, radioProps] = splitStateProps(props, USE_RADIO_STATE_KEYS);
+
+ const state = useRadioState(stateProps);
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default Radio;
+```
+
+[![Edit CodeSandbox](https://img.shields.io/badge/Radio%20Basic-Open%20On%20CodeSandbox-%230971f1?style=for-the-badge&logo=codesandbox&labelColor=151515)](https://codesandbox.io/s/juzxw)
+
+## Accessibility Requirement
+
+- Radio has role `radio`.
+- Radio has aria-checked set to true when it's checked. Otherwise, aria-checked
+ is set to false.
+- Radio extends the accessibility features of CompositeItem, which means it uses
+ the roving tabindex method to manage focus.
+- When Radio is not rendered as a native input checkbox, Radio will add
+ `role="radio"`
+- RadioGroup has role `radiogroup`.
+- RadioGroup must has `aria-label` or `aria-labelledby` to describe the group.
+
+## Composition
+
+- Radio uses [useCompositeItem](https://reakit.io/docs/composite)
+- RadioGroup uses [useComposite](https://reakit.io/docs/composite)
+
+## Props
+
+### `useRadioState`
+
+| Name | Type | Description |
+| :---------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **`baseId`** | string | ID that will serve as a base for all the items IDs. |
+| **`unstable_virtual`** ⚠️ | boolean | If enabled, the composite element will act as an[aria-activedescendant](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_focus_activedescendant)container instead of[roving tabindex](https://www.w3.org/TR/wai-aria-practices/#kbd_roving_tabindex).DOM focus will remain on the composite while its items receive virtual focus. |
+| **`rtl`** | boolean | Determines how `next` and `previous` functions will behave. If `rtl` isset to `true`, they will be inverted. This only affects the compositewidget behavior. You still need to set `dir="rtl"` on HTML/CSS. |
+| **`orientation`** | Orientation \| undefined | Defines the orientation of the composite widget. If the composite has asingle row or column (one-dimensional), the `orientation` value determineswhich arrow keys can be used to move focus: - `undefined`: all arrow keys work. - `horizontal`: only left and right arrow keys work. - `vertical`: only up and down arrow keys work.It doesn't have any effect on two-dimensional composites. |
+| **`currentId`** | string \| null \| undefined | The current focused item `id`. - `undefined` will automatically focus the first enabled composite item. - `null` will focus the base composite element and users will be able tonavigate out of it using arrow keys. - If `currentId` is initially set to `null`, the base composite elementitself will have focus and users will be able to navigate to it usingarrow keys. |
+| **`loop`** | boolean \| Orientation | On one-dimensional composites: - `true` loops from the last item to the first item and vice-versa. - `horizontal` loops only if `orientation` is `horizontal` or not set. - `vertical` loops only if `orientation` is `vertical` or not set. - If `currentId` is initially set to `null`, the composite element willbe focused in between the last and first items.On two-dimensional composites: - `true` loops from the last row/column item to the first item in thesame row/column and vice-versa. If it's the last item in the last row, itmoves to the first item in the first row and vice-versa. - `horizontal` loops only from the last row item to the first item inthe same row. - `vertical` loops only from the last column item to the first item inthe column row. - If `currentId` is initially set to `null`, vertical loop will have noeffect as moving down from the last row or up from the first row willfocus the composite element. - If `wrap` matches the value of `loop`, it'll wrap between the lastitem in the last row or column and the first item in the first row orcolumn and vice-versa. |
+| **`wrap`** | boolean \| Orientation | **Has effect only on two-dimensional composites**. If enabled, moving tothe next item from the last one in a row or column will focus the firstitem in the next row or column and vice-versa. - `true` wraps between rows and columns. - `horizontal` wraps only between rows. - `vertical` wraps only between columns. - If `loop` matches the value of `wrap`, it'll wrap between the lastitem in the last row or column and the first item in the first row orcolumn and vice-versa. |
+| **`shift`** | boolean | **Has effect only on two-dimensional composites**. If enabled, moving upor down when there's no next item or the next item is disabled will shiftto the item right before it. |
+| **`defaultState`** | string \| number \| null \| undefined | Default State of the Checkbox for uncontrolled Checkbox. |
+| **`state`** | string \| number \| null \| undefined | State of the Checkbox for controlled Checkbox.. |
+| **`onStateChange`** | Dispatch<SetStateAction<string \| number \| null>... | OnChange callback for controlled Checkbox. |
+
+### `Radio`
+
+| Name | Type | Description |
+| :-------------- | :-------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **`disabled`** | boolean \| undefined | Same as the HTML attribute. |
+| **`focusable`** | boolean \| undefined | When an element is `disabled`, it may still be `focusable`. It workssimilarly to `readOnly` on form elements. In this case, only`aria-disabled` will be set. |
+| **`id`** | string \| undefined | Same as the HTML attribute. |
+| **`value`** | string \| number | Same as the `value` attribute. |
+| **`checked`** | boolean \| undefined | Same as the `checked` attribute. |
+
+17 state props
+> These props are returned by the state hook. You can spread them into this component (`{...state}`) or pass them separately. You can also provide these props from your own state logic.
+
+| Name | Type | Description |
+| :---------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **`baseId`** | string | ID that will serve as a base for all the items IDs. |
+| **`unstable_virtual`** ⚠️ | boolean | If enabled, the composite element will act as an[aria-activedescendant](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_focus_activedescendant)container instead of[roving tabindex](https://www.w3.org/TR/wai-aria-practices/#kbd_roving_tabindex).DOM focus will remain on the composite while its items receive virtual focus. |
+| **`orientation`** | Orientation \| undefined | Defines the orientation of the composite widget. If the composite has asingle row or column (one-dimensional), the `orientation` value determineswhich arrow keys can be used to move focus: - `undefined`: all arrow keys work. - `horizontal`: only left and right arrow keys work. - `vertical`: only up and down arrow keys work.It doesn't have any effect on two-dimensional composites. |
+| **`unstable_moves`** ⚠️ | number | Stores the number of moves that have been performed by calling `move`,`next`, `previous`, `up`, `down`, `first` or `last`. |
+| **`currentId`** | string \| null \| undefined | The current focused item `id`. - `undefined` will automatically focus the first enabled composite item. - `null` will focus the base composite element and users will be able tonavigate out of it using arrow keys. - If `currentId` is initially set to `null`, the base composite elementitself will have focus and users will be able to navigate to it usingarrow keys. |
+| **`items`** | Item[] | Lists all the composite items with their `id`, DOM `ref`, `disabled` stateand `groupId` if any. This state is automatically updated when`registerItem` and `unregisterItem` are called. |
+| **`registerItem`** | (item: Item) => void | Registers a composite item. |
+| **`unregisterItem`** | (id: string) => void | Unregisters a composite item. |
+| **`setCurrentId`** | (value: SetStateAction<string \| null \| undefine... | Sets `currentId`. This is different from `composite.move` as this onlyupdates the `currentId` state without moving focus. When the compositewidget gets focused by the user, the item referred by the `currentId`state will get focus. |
+| **`next`** | (unstable_allTheWay?: boolean \| undefined) => void | Moves focus to the next item. |
+| **`previous`** | (unstable_allTheWay?: boolean \| undefined) => void | Moves focus to the previous item. |
+| **`up`** | (unstable_allTheWay?: boolean \| undefined) => void | Moves focus to the item above. |
+| **`down`** | (unstable_allTheWay?: boolean \| undefined) => void | Moves focus to the item below. |
+| **`first`** | () => void | Moves focus to the first item. |
+| **`last`** | () => void | Moves focus to the last item. |
+| **`state`** | string \| number \| null | The `value` attribute of the current checked radio. |
+| **`setState`** | (value: SetStateAction<string \| number \| null>)... | Sets `state`. |
+
+
+
+### `RadioGroup`
+
+| Name | Type | Description |
+| :-------------- | :-------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **`disabled`** | boolean \| undefined | Same as the HTML attribute. |
+| **`focusable`** | boolean \| undefined | When an element is `disabled`, it may still be `focusable`. It workssimilarly to `readOnly` on form elements. In this case, only`aria-disabled` will be set. |
+
+12 state props
+> These props are returned by the state hook. You can spread them into this component (`{...state}`) or pass them separately. You can also provide these props from your own state logic.
+
+| Name | Type | Description |
+| :---------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **`baseId`** | string | ID that will serve as a base for all the items IDs. |
+| **`unstable_virtual`** ⚠️ | boolean | If enabled, the composite element will act as an[aria-activedescendant](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_focus_activedescendant)container instead of[roving tabindex](https://www.w3.org/TR/wai-aria-practices/#kbd_roving_tabindex).DOM focus will remain on the composite while its items receive virtual focus. |
+| **`orientation`** | Orientation \| undefined | Defines the orientation of the composite widget. If the composite has asingle row or column (one-dimensional), the `orientation` value determineswhich arrow keys can be used to move focus: - `undefined`: all arrow keys work. - `horizontal`: only left and right arrow keys work. - `vertical`: only up and down arrow keys work.It doesn't have any effect on two-dimensional composites. |
+| **`currentId`** | string \| null \| undefined | The current focused item `id`. - `undefined` will automatically focus the first enabled composite item. - `null` will focus the base composite element and users will be able tonavigate out of it using arrow keys. - If `currentId` is initially set to `null`, the base composite elementitself will have focus and users will be able to navigate to it usingarrow keys. |
+| **`wrap`** | boolean \| Orientation | **Has effect only on two-dimensional composites**. If enabled, moving tothe next item from the last one in a row or column will focus the firstitem in the next row or column and vice-versa. - `true` wraps between rows and columns. - `horizontal` wraps only between rows. - `vertical` wraps only between columns. - If `loop` matches the value of `wrap`, it'll wrap between the lastitem in the last row or column and the first item in the first row orcolumn and vice-versa. |
+| **`unstable_moves`** ⚠️ | number | Stores the number of moves that have been performed by calling `move`,`next`, `previous`, `up`, `down`, `first` or `last`. |
+| **`groups`** | Group[] | Lists all the composite groups with their `id` and DOM `ref`. This stateis automatically updated when `registerGroup` and `unregisterGroup` arecalled. |
+| **`items`** | Item[] | Lists all the composite items with their `id`, DOM `ref`, `disabled` stateand `groupId` if any. This state is automatically updated when`registerItem` and `unregisterItem` are called. |
+| **`setCurrentId`** | (value: SetStateAction<string \| null \| undefine... | Sets `currentId`. This is different from `composite.move` as this onlyupdates the `currentId` state without moving focus. When the compositewidget gets focused by the user, the item referred by the `currentId`state will get focus. |
+| **`first`** | () => void | Moves focus to the first item. |
+| **`last`** | () => void | Moves focus to the last item. |
+| **`move`** | (id: string \| null) => void | Moves focus to a given item ID. |
+
+
diff --git a/src/index.ts b/src/index.ts
index a1a0fc12e..3d2f4636b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -10,6 +10,7 @@ export * from "./number-input";
export * from "./pagination";
export * from "./picker-base";
export * from "./progress";
+export * from "./radio";
export * from "./segment";
export * from "./select";
export * from "./slider";
diff --git a/src/radio/Radio.tsx b/src/radio/Radio.tsx
new file mode 100644
index 000000000..cd3017e4e
--- /dev/null
+++ b/src/radio/Radio.tsx
@@ -0,0 +1,149 @@
+import {
+ CompositeItemOptions,
+ CompositeItemHTMLProps,
+ useCompositeItem,
+} from "reakit";
+import * as React from "react";
+import { warning } from "reakit-warning/warning";
+import { useLiveRef, useForkRef } from "reakit-utils";
+import { createComponent, createHook } from "reakit-system";
+
+import { RADIO_KEYS } from "./__keys";
+import { RadioStateReturn } from "./RadioState";
+import { useInitialChecked, getChecked, fireChange } from "./helpers";
+
+export type RadioOptions = CompositeItemOptions &
+ Pick, "state" | "setState"> & {
+ /**
+ * Same as the `value` attribute.
+ */
+ value: string | number;
+ /**
+ * Same as the `checked` attribute.
+ */
+ checked?: boolean;
+ /**
+ * @private
+ */
+ unstable_checkOnFocus?: boolean;
+ };
+
+export type RadioHTMLProps = CompositeItemHTMLProps &
+ React.InputHTMLAttributes;
+
+export type RadioProps = RadioOptions & RadioHTMLProps;
+
+export const useRadio = createHook({
+ name: "Radio",
+ compose: useCompositeItem,
+ keys: RADIO_KEYS,
+
+ useOptions(
+ { unstable_clickOnEnter = false, unstable_checkOnFocus = true, ...options },
+ { value, checked },
+ ) {
+ return {
+ checked,
+ unstable_clickOnEnter,
+ unstable_checkOnFocus,
+ ...options,
+ value: options.value ?? value,
+ };
+ },
+
+ useProps(
+ options,
+ {
+ ref: htmlRef,
+ onChange: htmlOnChange,
+ onClick: htmlOnClick,
+ ...htmlProps
+ },
+ ) {
+ const {
+ currentId,
+ id,
+ disabled,
+ setState,
+ value,
+ unstable_moves,
+ unstable_checkOnFocus,
+ baseId,
+ } = options;
+ const ref = React.useRef(null);
+ const [isNativeRadio, setIsNativeRadio] = React.useState(true);
+ const checked = getChecked(options);
+ const isCurrentItemRef = useLiveRef(currentId === id);
+ const onChangeRef = useLiveRef(htmlOnChange);
+ const onClickRef = useLiveRef(htmlOnClick);
+
+ useInitialChecked(options);
+
+ React.useEffect(() => {
+ const element = ref.current;
+ if (!element) {
+ warning(
+ true,
+ "Can't determine whether the element is a native radio because `ref` wasn't passed to the component",
+ "See https://reakit.io/docs/radio",
+ );
+ return;
+ }
+ if (element.tagName !== "INPUT" || element.type !== "radio") {
+ setIsNativeRadio(false);
+ }
+ }, []);
+
+ const onChange = React.useCallback(
+ (event: React.ChangeEvent) => {
+ onChangeRef.current?.(event);
+
+ if (event.defaultPrevented) return;
+ if (disabled) return;
+
+ setState?.(value);
+ },
+ [disabled, onChangeRef, setState, value],
+ );
+
+ const onClick = React.useCallback(
+ (event: React.MouseEvent) => {
+ onClickRef.current?.(event);
+
+ if (event.defaultPrevented) return;
+ if (isNativeRadio) return;
+
+ fireChange(event.currentTarget, onChange);
+ },
+ [onClickRef, isNativeRadio, onChange],
+ );
+
+ React.useEffect(() => {
+ const element = ref.current;
+ if (!element) return;
+
+ if (unstable_moves && isCurrentItemRef.current && unstable_checkOnFocus) {
+ fireChange(element, onChange);
+ }
+ }, [unstable_moves, unstable_checkOnFocus, onChange, isCurrentItemRef]);
+
+ return {
+ ref: useForkRef(ref, htmlRef),
+ role: !isNativeRadio ? "radio" : undefined,
+ type: isNativeRadio ? "radio" : undefined,
+ value: isNativeRadio ? value : undefined,
+ name: isNativeRadio ? baseId : undefined,
+ "aria-checked": checked,
+ checked,
+ onChange,
+ onClick,
+ ...htmlProps,
+ };
+ },
+});
+
+export const Radio = createComponent({
+ as: "input",
+ memo: true,
+ useHook: useRadio,
+});
diff --git a/src/radio/RadioGroup.tsx b/src/radio/RadioGroup.tsx
new file mode 100644
index 000000000..6a2e0630e
--- /dev/null
+++ b/src/radio/RadioGroup.tsx
@@ -0,0 +1,36 @@
+import * as React from "react";
+import { useWarning } from "reakit-warning";
+import { CompositeOptions, CompositeHTMLProps, useComposite } from "reakit";
+import { createComponent, createHook, useCreateElement } from "reakit-system";
+
+import { RADIO_GROUP_KEYS } from "./__keys";
+
+export type RadioGroupOptions = CompositeOptions;
+
+export type RadioGroupHTMLProps = CompositeHTMLProps &
+ React.FieldsetHTMLAttributes;
+
+export type RadioGroupProps = RadioGroupOptions & RadioGroupHTMLProps;
+
+const useRadioGroup = createHook({
+ name: "RadioGroup",
+ compose: useComposite,
+ keys: RADIO_GROUP_KEYS,
+
+ useProps(options, htmlProps) {
+ return { role: "radiogroup", ...htmlProps };
+ },
+});
+
+export const RadioGroup = createComponent({
+ as: "div",
+ useHook: useRadioGroup,
+ useCreateElement: (type, props, children) => {
+ useWarning(
+ !props["aria-label"] && !props["aria-labelledby"],
+ "You should provide either `aria-label` or `aria-labelledby` props.",
+ "See https://reakit.io/docs/radio",
+ );
+ return useCreateElement(type, props, children);
+ },
+});
diff --git a/src/radio/RadioState.tsx b/src/radio/RadioState.tsx
new file mode 100644
index 000000000..42b942e73
--- /dev/null
+++ b/src/radio/RadioState.tsx
@@ -0,0 +1,65 @@
+import * as React from "react";
+import { useControllableState } from "../utils";
+import {
+ CompositeState,
+ CompositeActions,
+ CompositeInitialState,
+ useCompositeState,
+} from "reakit";
+
+export type RadioState = CompositeState & {
+ /**
+ * The `value` attribute of the current checked radio.
+ */
+ state: string | number | null;
+};
+
+export type RadioActions = CompositeActions & {
+ /**
+ * Sets `state`.
+ */
+ setState: React.Dispatch>;
+};
+
+export type RadioInitialState = CompositeInitialState & {
+ /**
+ * Default State of the Checkbox for uncontrolled Checkbox.
+ *
+ * @default false
+ */
+ defaultState?: RadioState["state"];
+
+ /**
+ * State of the Checkbox for controlled Checkbox..
+ */
+ state?: RadioState["state"];
+
+ /**
+ * OnChange callback for controlled Checkbox.
+ */
+ onStateChange?: RadioActions["setState"];
+};
+
+export type RadioStateReturn = RadioState & RadioActions;
+
+export function useRadioState(props: RadioInitialState): RadioStateReturn {
+ const {
+ defaultState,
+ state: stateProps,
+ onStateChange,
+ loop = true,
+ ...sealed
+ } = props;
+ const [state, setState] = useControllableState({
+ defaultValue: defaultState,
+ value: stateProps,
+ onChange: onStateChange,
+ });
+ const composite = useCompositeState({ ...sealed, loop });
+
+ return {
+ ...composite,
+ state,
+ setState,
+ };
+}
diff --git a/src/radio/__keys.ts b/src/radio/__keys.ts
new file mode 100644
index 000000000..7275a22fe
--- /dev/null
+++ b/src/radio/__keys.ts
@@ -0,0 +1,63 @@
+// Automatically generated
+export const USE_RADIO_STATE_KEYS = [
+ "baseId",
+ "unstable_virtual",
+ "rtl",
+ "orientation",
+ "currentId",
+ "loop",
+ "wrap",
+ "shift",
+ "unstable_includesBaseElement",
+ "defaultState",
+ "state",
+ "onStateChange",
+] as const;
+export const RADIO_STATE_KEYS = [
+ "baseId",
+ "unstable_idCountRef",
+ "unstable_virtual",
+ "rtl",
+ "orientation",
+ "items",
+ "groups",
+ "currentId",
+ "loop",
+ "wrap",
+ "shift",
+ "unstable_moves",
+ "unstable_hasActiveWidget",
+ "unstable_includesBaseElement",
+ "state",
+ "setBaseId",
+ "registerItem",
+ "unregisterItem",
+ "registerGroup",
+ "unregisterGroup",
+ "move",
+ "next",
+ "previous",
+ "up",
+ "down",
+ "first",
+ "last",
+ "sort",
+ "unstable_setVirtual",
+ "setRTL",
+ "setOrientation",
+ "setCurrentId",
+ "setLoop",
+ "setWrap",
+ "setShift",
+ "reset",
+ "unstable_setIncludesBaseElement",
+ "unstable_setHasActiveWidget",
+ "setState",
+] as const;
+export const RADIO_KEYS = [
+ ...RADIO_STATE_KEYS,
+ "value",
+ "checked",
+ "unstable_checkOnFocus",
+] as const;
+export const RADIO_GROUP_KEYS = RADIO_STATE_KEYS;
diff --git a/src/radio/helpers.tsx b/src/radio/helpers.tsx
new file mode 100644
index 000000000..2fa337b1d
--- /dev/null
+++ b/src/radio/helpers.tsx
@@ -0,0 +1,37 @@
+import React from "react";
+import { createEvent } from "reakit-utils";
+
+import { RadioOptions } from "./Radio";
+
+export function getChecked(options: RadioOptions) {
+ const { checked, value, state } = options;
+ if (typeof checked !== "undefined") return checked;
+
+ return typeof value !== "undefined" && state === value;
+}
+
+export function useInitialChecked(options: RadioOptions) {
+ const [initialChecked] = React.useState(() => getChecked(options));
+ const { id, currentId, setCurrentId } = options;
+ const [initialCurrentId] = React.useState(currentId);
+
+ React.useEffect(() => {
+ if (initialChecked && id && initialCurrentId !== id) {
+ setCurrentId?.(id);
+ }
+ }, [initialChecked, id, setCurrentId, initialCurrentId]);
+}
+
+export function fireChange(
+ element: HTMLElement,
+ onChange?: React.ChangeEventHandler,
+) {
+ const event = createEvent(element, "change");
+
+ Object.defineProperties(event, {
+ type: { value: "change" },
+ target: { value: element },
+ currentTarget: { value: element },
+ });
+ onChange?.(event as any);
+}
diff --git a/src/radio/index.ts b/src/radio/index.ts
new file mode 100644
index 000000000..8f62f1796
--- /dev/null
+++ b/src/radio/index.ts
@@ -0,0 +1,4 @@
+export * from "./Radio";
+export * from "./RadioGroup";
+export * from "./RadioState";
+export * from "./__keys";
diff --git a/src/radio/stories/RadioBasic.component.tsx b/src/radio/stories/RadioBasic.component.tsx
new file mode 100644
index 000000000..42767fad3
--- /dev/null
+++ b/src/radio/stories/RadioBasic.component.tsx
@@ -0,0 +1,38 @@
+import * as React from "react";
+
+import {
+ useRadioState,
+ RadioInitialState,
+ Radio as RenderlesskitRadio,
+ RadioProps as RenderlesskitRadioProps,
+ USE_RADIO_STATE_KEYS,
+ splitStateProps,
+ RadioGroup,
+} from "../../index";
+
+export type RadioProps = RenderlesskitRadioProps & RadioInitialState & {};
+
+export const Radio: React.FC = props => {
+ const [stateProps, radioProps] = splitStateProps<
+ RadioInitialState,
+ RadioProps
+ >(props, USE_RADIO_STATE_KEYS);
+
+ const state = useRadioState(stateProps);
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default Radio;
diff --git a/src/radio/stories/RadioBasic.stories.tsx b/src/radio/stories/RadioBasic.stories.tsx
new file mode 100644
index 000000000..f27bcd3b0
--- /dev/null
+++ b/src/radio/stories/RadioBasic.stories.tsx
@@ -0,0 +1,33 @@
+import * as React from "react";
+import { Meta, Story } from "@storybook/react";
+
+import js from "./templates/RadioBasicJsx";
+import ts from "./templates/RadioBasicTsx";
+import { Radio, RadioProps } from "./RadioBasic.component";
+import { createControls, createPreviewTabs } from "../../../.storybook/utils";
+
+export default {
+ component: Radio,
+ title: "Radio/Basic",
+ parameters: {
+ layout: "centered",
+ preview: createPreviewTabs({ js, ts }),
+ },
+ argTypes: createControls({
+ ignore: [
+ "unstable_system",
+ "unstable_clickOnEnter",
+ "unstable_clickOnSpace",
+ "wrapElement",
+ "focusable",
+ "as",
+ "checked",
+ "state",
+ "setState",
+ "onStateChange",
+ "value",
+ ],
+ }),
+} as Meta;
+
+export const Default: Story = args => ;
diff --git a/yarn.lock b/yarn.lock
index 612c270ba..7357098ec 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5715,11 +5715,6 @@ babel-plugin-apply-mdx-type-prop@1.6.22:
"@babel/helper-plugin-utils" "7.10.4"
"@mdx-js/util" "1.6.22"
-babel-plugin-date-fns@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-date-fns/-/babel-plugin-date-fns-2.0.0.tgz#56074f1b4659c3b1208b5d156d4f51612d7af620"
- integrity sha512-MbsQzEgglAIBZLQbKQDgMUgFDwf7sSHXgaWRXowiEVs1B+eiBge4JnhBQtIaHIVLE9QmXfDQbb18oggvP7KSFQ==
-
babel-plugin-dynamic-import-node@^2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3"