From 73db6987319941036852437d0750d67ec1f377c5 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Wed, 30 Apr 2025 09:28:32 -0700 Subject: [PATCH 1/9] radio-group: Add unstable `ItemRoot`, `ItemTrigger` and `ItemBubbleInput` --- packages/react/radio-group/src/index.ts | 15 +- .../react/radio-group/src/radio-group.tsx | 253 ++++++++++++----- packages/react/radio-group/src/radio.tsx | 256 ++++++++++++------ 3 files changed, 368 insertions(+), 156 deletions(-) diff --git a/packages/react/radio-group/src/index.ts b/packages/react/radio-group/src/index.ts index af4c9afd1..843c32c43 100644 --- a/packages/react/radio-group/src/index.ts +++ b/packages/react/radio-group/src/index.ts @@ -4,10 +4,23 @@ export { // RadioGroup, RadioGroupItem, + RadioGroupItemRoot as unstable_RadioGroupItemRoot, + RadioGroupItemTrigger as unstable_RadioGroupItemTrigger, + RadioGroupItemBubbleInput as unstable_RadioGroupItemBubbleInput, RadioGroupIndicator, // Root, Item, + ItemRoot as unstable_ItemRoot, + ItemTrigger as unstable_ItemTrigger, + ItemBubbleInput as unstable_ItemBubbleInput, Indicator, } from './radio-group'; -export type { RadioGroupProps, RadioGroupItemProps, RadioGroupIndicatorProps } from './radio-group'; +export type { + RadioGroupProps, + RadioGroupItemProps, + RadioGroupItemRootProps as unstable_RadioGroupItemRootProps, + RadioGroupItemTriggerProps as unstable_RadioGroupItemTriggerProps, + RadioGroupItemBubbleInputProps as unstable_RadioGroupItemBubbleInputProps, + RadioGroupIndicatorProps, +} from './radio-group'; diff --git a/packages/react/radio-group/src/radio-group.tsx b/packages/react/radio-group/src/radio-group.tsx index 6fb5ff8ff..fac741066 100644 --- a/packages/react/radio-group/src/radio-group.tsx +++ b/packages/react/radio-group/src/radio-group.tsx @@ -7,7 +7,16 @@ import * as RovingFocusGroup from '@radix-ui/react-roving-focus'; import { createRovingFocusGroupScope } from '@radix-ui/react-roving-focus'; import { useControllableState } from '@radix-ui/react-use-controllable-state'; import { useDirection } from '@radix-ui/react-direction'; -import { Radio, RadioIndicator, createRadioScope } from './radio'; +import { + RadioProvider, + RadioTrigger, + RadioBubbleInput, + type RadioBubbleInputProps, + RadioIndicator, + createRadioScope, + useRadioContext, + useInternalRadioScope, +} from './radio'; import type { Scope } from '@radix-ui/react-context'; @@ -41,9 +50,9 @@ type RadioGroupElement = React.ElementRef; type RovingFocusGroupProps = React.ComponentPropsWithoutRef; type PrimitiveDivProps = React.ComponentPropsWithoutRef; interface RadioGroupProps extends PrimitiveDivProps { - name?: RadioGroupContextValue['name']; - required?: React.ComponentPropsWithoutRef['required']; - disabled?: React.ComponentPropsWithoutRef['disabled']; + name?: string; + required?: boolean; + disabled?: boolean; dir?: RovingFocusGroupProps['dir']; orientation?: RovingFocusGroupProps['orientation']; loop?: RovingFocusGroupProps['loop']; @@ -109,75 +118,176 @@ const RadioGroup = React.forwardRef( RadioGroup.displayName = RADIO_GROUP_NAME; +/* ------------------------------------------------------------------------------------------------- + * RadioGroupItemRoot + * -----------------------------------------------------------------------------------------------*/ + +const ITEM_ROOT_NAME = 'RadioGroupItemRoot'; + +type RadioProviderProps = React.ComponentPropsWithoutRef; +interface RadioGroupItemRootProps extends Omit { + value: string; +} + +const RadioGroupItemRoot = (props: ScopedProps) => { + const { + __scopeRadioGroup, + disabled, + children, + checked, + // @ts-expect-error + internal_do_not_use_render, + ...itemProps + } = props; + const context = useRadioGroupContext(ITEM_ROOT_NAME, __scopeRadioGroup); + const radioScope = useRadioScope(__scopeRadioGroup); + + return ( + { + return typeof internal_do_not_use_render === 'function' + ? internal_do_not_use_render({ ...context, isFormControl }) + : children; + }} + /> + ); +}; + +RadioGroupItemRoot.displayName = ITEM_ROOT_NAME; + +/* ------------------------------------------------------------------------------------------------- + * RadioGroupItemTrigger + * -----------------------------------------------------------------------------------------------*/ + +const ITEM_TRIGGER_NAME = 'RadioGroupItemTrigger'; + +type RadioGroupItemTriggerElement = React.ElementRef; +type RadioTriggerProps = React.ComponentPropsWithoutRef; +interface RadioGroupItemTriggerProps + extends Omit {} + +const RadioGroupItemTrigger = React.forwardRef< + RadioGroupItemTriggerElement, + RadioGroupItemTriggerProps +>((props: ScopedProps, forwardedRef) => { + const { __scopeRadioGroup, ...itemProps } = props; + const context = useRadioGroupContext(ITEM_TRIGGER_NAME, __scopeRadioGroup); + const radioScope = useRadioScope(__scopeRadioGroup); + const { + checked, + disabled: isDisabled, + value, + } = useRadioContext(ITEM_TRIGGER_NAME, useInternalRadioScope(ITEM_TRIGGER_NAME)); + + const rovingFocusGroupScope = useRovingFocusGroupScope(__scopeRadioGroup); + + const ref = React.useRef(null); + const composedRefs = useComposedRefs(forwardedRef, ref); + const isArrowKeyPressedRef = React.useRef(false); + + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (ARROW_KEYS.includes(event.key)) { + isArrowKeyPressedRef.current = true; + } + }; + const handleKeyUp = () => (isArrowKeyPressedRef.current = false); + document.addEventListener('keydown', handleKeyDown); + document.addEventListener('keyup', handleKeyUp); + return () => { + document.removeEventListener('keydown', handleKeyDown); + document.removeEventListener('keyup', handleKeyUp); + }; + }, []); + + return ( + + context.onValueChange(value)} + onKeyDown={composeEventHandlers((event) => { + // According to WAI ARIA, radio groups don't activate items on enter keypress + if (event.key === 'Enter') event.preventDefault(); + })} + onFocus={composeEventHandlers(itemProps.onFocus, () => { + /** + * Our `RovingFocusGroup` will focus the radio when navigating with arrow keys + * and we need to "check" it in that case. We click it to "check" it (instead + * of updating `context.value`) so that the radio change event fires. + */ + if (isArrowKeyPressedRef.current) { + ref.current?.click(); + } + })} + /> + + ); +}); + +RadioGroupItemTrigger.displayName = ITEM_TRIGGER_NAME; + /* ------------------------------------------------------------------------------------------------- * RadioGroupItem * -----------------------------------------------------------------------------------------------*/ const ITEM_NAME = 'RadioGroupItem'; -type RadioGroupItemElement = React.ElementRef; -type RadioProps = React.ComponentPropsWithoutRef; -interface RadioGroupItemProps extends Omit { +type RadioGroupItemElement = React.ElementRef; + +interface RadioGroupItemProps + extends Omit, 'name'> { + checked?: boolean; + required?: boolean; + form?: string; + disabled?: boolean; value: string; + children?: React.ReactNode; + onCheck?: () => void; } const RadioGroupItem = React.forwardRef( (props: ScopedProps, forwardedRef) => { - const { __scopeRadioGroup, disabled, ...itemProps } = props; - const context = useRadioGroupContext(ITEM_NAME, __scopeRadioGroup); - const isDisabled = context.disabled || disabled; - const rovingFocusGroupScope = useRovingFocusGroupScope(__scopeRadioGroup); + const { + __scopeRadioGroup, + disabled, + value, + checked: checkedProp, + form, + required, + ...triggerProps + } = props; + const radioScope = useRadioScope(__scopeRadioGroup); - const ref = React.useRef>(null); - const composedRefs = useComposedRefs(forwardedRef, ref); - const checked = context.value === itemProps.value; - const isArrowKeyPressedRef = React.useRef(false); - - React.useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if (ARROW_KEYS.includes(event.key)) { - isArrowKeyPressedRef.current = true; - } - }; - const handleKeyUp = () => (isArrowKeyPressedRef.current = false); - document.addEventListener('keydown', handleKeyDown); - document.addEventListener('keyup', handleKeyUp); - return () => { - document.removeEventListener('keydown', handleKeyDown); - document.removeEventListener('keyup', handleKeyUp); - }; - }, []); return ( - - context.onValueChange(itemProps.value)} - onKeyDown={composeEventHandlers((event) => { - // According to WAI ARIA, radio groups don't activate items on enter keypress - if (event.key === 'Enter') event.preventDefault(); - })} - onFocus={composeEventHandlers(itemProps.onFocus, () => { - /** - * Our `RovingFocusGroup` will focus the radio when navigating with arrow keys - * and we need to "check" it in that case. We click it to "check" it (instead - * of updating `context.value`) so that the radio change event fires. - */ - if (isArrowKeyPressedRef.current) ref.current?.click(); - })} - /> - + ( + <> + + {isFormControl && } + + )} + /> ); } ); @@ -206,19 +316,28 @@ RadioGroupIndicator.displayName = INDICATOR_NAME; /* ---------------------------------------------------------------------------------------------- */ -const Root = RadioGroup; -const Item = RadioGroupItem; -const Indicator = RadioGroupIndicator; - export { createRadioGroupScope, // RadioGroup, + RadioGroupItemRoot, + RadioGroupItemTrigger, + RadioBubbleInput as RadioGroupItemBubbleInput, RadioGroupItem, RadioGroupIndicator, // - Root, - Item, - Indicator, + RadioGroup as Root, + RadioGroupItemRoot as ItemRoot, + RadioGroupItemTrigger as ItemTrigger, + RadioBubbleInput as ItemBubbleInput, + RadioGroupItem as Item, + RadioGroupIndicator as Indicator, +}; +export type { + RadioGroupProps, + RadioGroupItemRootProps, + RadioGroupItemTriggerProps, + RadioBubbleInputProps as RadioGroupItemBubbleInputProps, + RadioGroupItemProps, + RadioGroupIndicatorProps, }; -export type { RadioGroupProps, RadioGroupItemProps, RadioGroupIndicatorProps }; diff --git a/packages/react/radio-group/src/radio.tsx b/packages/react/radio-group/src/radio.tsx index 25009d3fd..dc4227761 100644 --- a/packages/react/radio-group/src/radio.tsx +++ b/packages/react/radio-group/src/radio.tsx @@ -18,82 +18,142 @@ const RADIO_NAME = 'Radio'; type ScopedProps

= P & { __scopeRadio?: Scope }; const [createRadioContext, createRadioScope] = createContextScope(RADIO_NAME); -type RadioContextValue = { checked: boolean; disabled?: boolean }; -const [RadioProvider, useRadioContext] = createRadioContext(RADIO_NAME); +interface RadioContextValue { + checked: boolean; + disabled: boolean | undefined; + button: HTMLButtonElement | null; + setButton: React.Dispatch>; + name: string | undefined; + form: string | undefined; + value: string; + hasConsumerStoppedPropagationRef: React.RefObject; + required: boolean | undefined; + isFormControl: boolean; + bubbleInput: HTMLInputElement | null; + setBubbleInput: React.Dispatch>; +} +const [RadioProviderImpl, useRadioContext] = createRadioContext(RADIO_NAME); + +/* ------------------------------------------------------------------------------------------------- + * RadioProvider + * -----------------------------------------------------------------------------------------------*/ + +const RadioScopeContext = React.createContext(null); +RadioScopeContext.displayName = 'RadioScopeContext'; + +function useInternalRadioScope(displayName: string): Scope { + const scope = React.useContext(RadioScopeContext); + if (!scope) { + throw new Error( + `[${displayName}] is missing a parent component. Make sure to wrap your component tree with .` + ); + } + return scope; +} -type RadioElement = React.ElementRef; -type PrimitiveButtonProps = React.ComponentPropsWithoutRef; -interface RadioProps extends PrimitiveButtonProps { +interface RadioProviderProps { checked?: boolean; required?: boolean; - onCheck?(): void; + name?: string; + form?: string; + disabled?: boolean; + value?: string; + children?: React.ReactNode; } -const Radio = React.forwardRef( - (props: ScopedProps, forwardedRef) => { - const { - __scopeRadio, - name, - checked = false, - required, - disabled, - value = 'on', - onCheck, - form, - ...radioProps - } = props; - const [button, setButton] = React.useState(null); - const composedRefs = useComposedRefs(forwardedRef, (node) => setButton(node)); - const hasConsumerStoppedPropagationRef = React.useRef(false); - // We set this to true by default so that events bubble to forms without JS (SSR) - const isFormControl = button ? form || !!button.closest('form') : true; +const RadioProvider = (props: ScopedProps) => { + const { + __scopeRadio, + name, + checked = false, + required, + disabled, + value = 'on', + form, + children, + // @ts-expect-error + internal_do_not_use_render, + } = props; + const [button, setButton] = React.useState(null); + const [bubbleInput, setBubbleInput] = React.useState(null); + const hasConsumerStoppedPropagationRef = React.useRef(false); + const isFormControl = button + ? !!form || !!button.closest('form') + : // We set this to true by default so that events bubble to forms without JS (SSR) + true; + + const context: RadioContextValue = { + checked, + disabled, + button, + setButton, + name, + form, + value, + hasConsumerStoppedPropagationRef, + required, + isFormControl, + bubbleInput, + setBubbleInput, + }; + + return ( + + + {typeof internal_do_not_use_render === 'function' + ? internal_do_not_use_render(context) + : children} + + + ); +}; + +/* ------------------------------------------------------------------------------------------------- + * RadioTrigger + * -----------------------------------------------------------------------------------------------*/ + +const TRIGGER_NAME = 'RadioTrigger'; + +interface RadioTriggerProps + extends Omit, keyof RadioProviderProps> { + children?: React.ReactNode; + onCheck?: () => void; +} + +const RadioTrigger = React.forwardRef( + ({ __scopeRadio, onCheck, ...radioProps }: ScopedProps, forwardedRef) => { + const { value, disabled, checked, setButton, hasConsumerStoppedPropagationRef, isFormControl } = + useRadioContext(TRIGGER_NAME, __scopeRadio); + const composedRefs = useComposedRefs(forwardedRef, setButton); return ( - - { - // radios cannot be unchecked so we only communicate a checked state - if (!checked) onCheck?.(); - if (isFormControl) { - hasConsumerStoppedPropagationRef.current = event.isPropagationStopped(); - // if radio is in a form, stop propagation from the button so that we only propagate - // one click event (from the input). We propagate changes from an input so that native - // form validation works and form events reflect radio updates. - if (!hasConsumerStoppedPropagationRef.current) event.stopPropagation(); - } - })} - /> - {isFormControl && ( - - )} - + { + // radios cannot be unchecked so we only communicate a checked state + if (!checked) onCheck?.(); + if (isFormControl) { + hasConsumerStoppedPropagationRef.current = event.isPropagationStopped(); + // if radio is in a form, stop propagation from the button so that we only propagate + // one click event (from the input). We propagate changes from an input so that native + // form validation works and form events reflect radio updates. + if (!hasConsumerStoppedPropagationRef.current) event.stopPropagation(); + } + })} + /> ); } ); -Radio.displayName = RADIO_NAME; +RadioTrigger.displayName = TRIGGER_NAME; /* ------------------------------------------------------------------------------------------------- * RadioIndicator @@ -103,7 +163,7 @@ const INDICATOR_NAME = 'RadioIndicator'; type RadioIndicatorElement = React.ElementRef; type PrimitiveSpanProps = React.ComponentPropsWithoutRef; -export interface RadioIndicatorProps extends PrimitiveSpanProps { +interface RadioIndicatorProps extends PrimitiveSpanProps { /** * Used to force mounting when more control is needed. Useful when * controlling animation with React animation libraries. @@ -137,31 +197,30 @@ RadioIndicator.displayName = INDICATOR_NAME; const BUBBLE_INPUT_NAME = 'RadioBubbleInput'; type InputProps = React.ComponentPropsWithoutRef; -interface RadioBubbleInputProps extends Omit { - checked: boolean; - control: HTMLElement | null; - bubbles: boolean; -} +interface RadioBubbleInputProps extends Omit {} const RadioBubbleInput = React.forwardRef( - ( - { - __scopeRadio, - control, + ({ __scopeRadio, ...props }: ScopedProps, forwardedRef) => { + const { + button, + hasConsumerStoppedPropagationRef, checked, - bubbles = true, - ...props - }: ScopedProps, - forwardedRef - ) => { - const ref = React.useRef(null); - const composedRefs = useComposedRefs(ref, forwardedRef); + required, + disabled, + name, + value, + form, + bubbleInput, + setBubbleInput, + } = useRadioContext(BUBBLE_INPUT_NAME, __scopeRadio); + + const composedRefs = useComposedRefs(forwardedRef, setBubbleInput); const prevChecked = usePrevious(checked); - const controlSize = useSize(control); + const controlSize = useSize(button); // Bubble checked change to parents (e.g form change event) React.useEffect(() => { - const input = ref.current; + const input = bubbleInput; if (!input) return; const inputProto = window.HTMLInputElement.prototype; @@ -171,17 +230,22 @@ const RadioBubbleInput = React.forwardRef Date: Wed, 30 Apr 2025 09:33:40 -0700 Subject: [PATCH 2/9] rename RadioProvider to RadioRoot --- .../react/radio-group/src/radio-group.tsx | 6 +++--- packages/react/radio-group/src/radio.tsx | 20 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/react/radio-group/src/radio-group.tsx b/packages/react/radio-group/src/radio-group.tsx index fac741066..725a59481 100644 --- a/packages/react/radio-group/src/radio-group.tsx +++ b/packages/react/radio-group/src/radio-group.tsx @@ -8,7 +8,7 @@ import { createRovingFocusGroupScope } from '@radix-ui/react-roving-focus'; import { useControllableState } from '@radix-ui/react-use-controllable-state'; import { useDirection } from '@radix-ui/react-direction'; import { - RadioProvider, + RadioRoot, RadioTrigger, RadioBubbleInput, type RadioBubbleInputProps, @@ -124,7 +124,7 @@ RadioGroup.displayName = RADIO_GROUP_NAME; const ITEM_ROOT_NAME = 'RadioGroupItemRoot'; -type RadioProviderProps = React.ComponentPropsWithoutRef; +type RadioProviderProps = React.ComponentPropsWithoutRef; interface RadioGroupItemRootProps extends Omit { value: string; } @@ -143,7 +143,7 @@ const RadioGroupItemRoot = (props: ScopedProps) => { const radioScope = useRadioScope(__scopeRadioGroup); return ( - >; } -const [RadioProviderImpl, useRadioContext] = createRadioContext(RADIO_NAME); +const [RadioProvider, useRadioContext] = createRadioContext(RADIO_NAME); /* ------------------------------------------------------------------------------------------------- - * RadioProvider + * RadioRoot * -----------------------------------------------------------------------------------------------*/ const RadioScopeContext = React.createContext(null); @@ -51,7 +51,7 @@ function useInternalRadioScope(displayName: string): Scope { return scope; } -interface RadioProviderProps { +interface RadioRootProps { checked?: boolean; required?: boolean; name?: string; @@ -61,7 +61,7 @@ interface RadioProviderProps { children?: React.ReactNode; } -const RadioProvider = (props: ScopedProps) => { +const RadioRoot = (props: ScopedProps) => { const { __scopeRadio, name, @@ -99,11 +99,11 @@ const RadioProvider = (props: ScopedProps) => { return ( - + {typeof internal_do_not_use_render === 'function' ? internal_do_not_use_render(context) : children} - + ); }; @@ -115,7 +115,7 @@ const RadioProvider = (props: ScopedProps) => { const TRIGGER_NAME = 'RadioTrigger'; interface RadioTriggerProps - extends Omit, keyof RadioProviderProps> { + extends Omit, keyof RadioRootProps> { children?: React.ReactNode; onCheck?: () => void; } @@ -277,19 +277,19 @@ export { // RadioIndicator, RadioTrigger, - RadioProvider, + RadioRoot, RadioBubbleInput, // RadioTrigger as Trigger, RadioIndicator as Indicator, - RadioProvider as Provider, + RadioRoot as Root, RadioBubbleInput as BubbleInput, }; export type { RadioTriggerProps, RadioIndicatorProps, - RadioProviderProps, + RadioRootProps, RadioBubbleInputProps, RadioContextValue, }; From 69e5ea23c51c450ff31fc8c50c8ce951ad7201b5 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Wed, 30 Apr 2025 13:45:33 -0700 Subject: [PATCH 3/9] fix bubble inputs --- .../react/radio-group/src/radio-group.tsx | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/react/radio-group/src/radio-group.tsx b/packages/react/radio-group/src/radio-group.tsx index 725a59481..bc91ceef2 100644 --- a/packages/react/radio-group/src/radio-group.tsx +++ b/packages/react/radio-group/src/radio-group.tsx @@ -11,7 +11,6 @@ import { RadioRoot, RadioTrigger, RadioBubbleInput, - type RadioBubbleInputProps, RadioIndicator, createRadioScope, useRadioContext, @@ -240,6 +239,27 @@ const RadioGroupItemTrigger = React.forwardRef< RadioGroupItemTrigger.displayName = ITEM_TRIGGER_NAME; +/* ------------------------------------------------------------------------------------------------- + * RadioGroupItemBubbleInput + * -----------------------------------------------------------------------------------------------*/ + +const ITEM_BUBBLE_INPUT_NAME = 'RadioGroupItemBubbleInput'; + +type RadioGroupItemBubbleInputElement = HTMLInputElement; +type RadioBubbleInputProps = React.ComponentPropsWithoutRef; +interface RadioGroupItemBubbleInputProps extends RadioBubbleInputProps {} + +const RadioGroupItemBubbleInput = React.forwardRef< + RadioGroupItemBubbleInputElement, + RadioGroupItemBubbleInputProps +>((props: ScopedProps, forwardedRef) => { + const { __scopeRadioGroup, ...inputProps } = props; + const radioScope = useRadioScope(__scopeRadioGroup); + return ; +}); + +RadioGroupItemBubbleInput.displayName = ITEM_BUBBLE_INPUT_NAME; + /* ------------------------------------------------------------------------------------------------- * RadioGroupItem * -----------------------------------------------------------------------------------------------*/ @@ -322,14 +342,14 @@ export { RadioGroup, RadioGroupItemRoot, RadioGroupItemTrigger, - RadioBubbleInput as RadioGroupItemBubbleInput, + RadioGroupItemBubbleInput, RadioGroupItem, RadioGroupIndicator, // RadioGroup as Root, RadioGroupItemRoot as ItemRoot, RadioGroupItemTrigger as ItemTrigger, - RadioBubbleInput as ItemBubbleInput, + RadioGroupItemBubbleInput as ItemBubbleInput, RadioGroupItem as Item, RadioGroupIndicator as Indicator, }; @@ -337,7 +357,7 @@ export type { RadioGroupProps, RadioGroupItemRootProps, RadioGroupItemTriggerProps, - RadioBubbleInputProps as RadioGroupItemBubbleInputProps, + RadioGroupItemBubbleInputProps, RadioGroupItemProps, RadioGroupIndicatorProps, }; From ab47442e8369642421f48f344568939abea45c15 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Wed, 30 Apr 2025 13:47:16 -0700 Subject: [PATCH 4/9] add stories --- .../stories/radio-group-legacy.stories.tsx | 420 ++++++++++++ .../storybook/stories/radio-group.stories.tsx | 603 +++++++++++------- 2 files changed, 796 insertions(+), 227 deletions(-) create mode 100644 apps/storybook/stories/radio-group-legacy.stories.tsx diff --git a/apps/storybook/stories/radio-group-legacy.stories.tsx b/apps/storybook/stories/radio-group-legacy.stories.tsx new file mode 100644 index 000000000..fda848fe5 --- /dev/null +++ b/apps/storybook/stories/radio-group-legacy.stories.tsx @@ -0,0 +1,420 @@ +import * as React from 'react'; +import type { Meta } from '@storybook/react'; +import { Direction, Label as LabelPrimitive, RadioGroup } from 'radix-ui'; +import styles from './radio-group.stories.module.css'; + +export default { + title: 'Components/RadioGroup (legacy API)', + component: RadioGroup.Root, +} satisfies Meta; + +export const Styled = () => ( + +); + +export const Controlled = () => { + const [value, setValue] = React.useState('2'); + + return ( + + + + + + + + + + + + ); +}; + +export const Unset = () => ( + +); + +export const WithinForm = () => { + const [data, setData] = React.useState({ optional: '', required: '', stopprop: '' }); + + return ( +

event.preventDefault()} + onChange={(event) => { + const radio = event.target as HTMLInputElement; + setData((prevData) => ({ ...prevData, [radio.name]: radio.value })); + }} + > +
+ optional value: {data.optional} + + + + + + + + + + + +
+ +
+
+ +
+ required value: {data.required} + + + + + + + + + + + +
+ +
+
+ +
+ stop propagation value: {data.stopprop} + + event.stopPropagation()} + > + + + event.stopPropagation()} + > + + + event.stopPropagation()} + > + + + +
+ +
+
+ + +
+ ); +}; + +export const Animated = () => { + const indicatorClass = [styles.indicator, styles.animatedIndicator].join(' '); + return ( + + ); +}; + +export const Chromatic = () => { + const manualFocusRef = React.useRef>(null); + + React.useEffect(() => { + manualFocusRef.current?.focus(); + }, []); + + return ( + <> +

Uncontrolled

+

Unset

+ + + + + + + + + + + + +

Set

+ + + + + + + + + + + + +

Controlled

+

Unset

+ + + + + + + + + + + + +

Set

+ + + + + + + + + + + + +

Disabled item

+ + + + + + + + + + + + +

Disabled root

+ + + + + {/* Not possible to set `disabled` back to `false` since it's set on the root (this item + should still be disabled). */} + + + + + + + + +

All items disabled

+ + + + + + + + + + + + +

Manual focus into group

+ + + + + + + + + + + + +

Force mounted indicator

+ + + + + + + + + + + + +

Direction

+

Prop

+ + + + + + + + + + + + +

Inherited

+ + + + + + + + + + + + + + +

State attributes

+

Default

+ + + + + + + + + + + + +

Disabled item

+ + + + + + + + + + + + + + + + + + + + + + + + +

Disabled root

+ + + + + + + + + + + + +

All items disabled

+ + + + + + + + + + + + + ); +}; +Chromatic.parameters = { chromatic: { disable: false } }; + +const Label = (props: any) => ; diff --git a/apps/storybook/stories/radio-group.stories.tsx b/apps/storybook/stories/radio-group.stories.tsx index 77ba95650..8a4372acb 100644 --- a/apps/storybook/stories/radio-group.stories.tsx +++ b/apps/storybook/stories/radio-group.stories.tsx @@ -1,80 +1,103 @@ +/* eslint-disable react/jsx-pascal-case */ import * as React from 'react'; +import type { Meta } from '@storybook/react'; import { Direction, Label as LabelPrimitive, RadioGroup } from 'radix-ui'; import styles from './radio-group.stories.module.css'; -export default { title: 'Components/RadioGroup' }; +export default { + title: 'Components/RadioGroup', + component: RadioGroup.Root, +} satisfies Meta; -export const LegacyStyled = () => ( +export const Styled = () => ( ); -export const LegacyControlled = () => { +export const Controlled = () => { const [value, setValue] = React.useState('2'); return ( - - - - - - - - - + + + + + + + + + + + + + + + ); }; -export const LegacyUnset = () => ( +export const Unset = () => ( ); -export const LegacyWithinForm = () => { +export const WithinForm = () => { const [data, setData] = React.useState({ optional: '', required: '', stopprop: '' }); return ( @@ -88,15 +111,24 @@ export const LegacyWithinForm = () => {
optional value: {data.optional} - - - - - - - - - + + + + + + + + + + + + + + + + + +
@@ -106,15 +138,24 @@ export const LegacyWithinForm = () => {
required value: {data.required} - - - - - - - - - + + + + + + + + + + + + + + + + + +
@@ -124,27 +165,33 @@ export const LegacyWithinForm = () => {
stop propagation value: {data.stopprop} - event.stopPropagation()} - > - - - event.stopPropagation()} - > - - - event.stopPropagation()} - > - - + + event.stopPropagation()} + className={styles.item} + > + + + + + + event.stopPropagation()} + className={styles.item} + > + + + + + + event.stopPropagation()} + className={styles.item} + > + + + +
@@ -156,28 +203,34 @@ export const LegacyWithinForm = () => { ); }; -export const LegacyAnimated = () => { +export const Animated = () => { const indicatorClass = [styles.indicator, styles.animatedIndicator].join(' '); return (