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"