diff --git a/examples/react-native/storybook/stories/RadioGroup.stories.tsx b/examples/react-native/storybook/stories/RadioGroup.stories.tsx index 99b3a8f6100..138c0aa146e 100644 --- a/examples/react-native/storybook/stories/RadioGroup.stories.tsx +++ b/examples/react-native/storybook/stories/RadioGroup.stories.tsx @@ -13,7 +13,23 @@ const ControlledRadioGroup = ({ ...props }: any) => { }; return ( - + + + + + + ); +}; + +const UncontrolledRadioGroup = ({ ...props }: any) => { + const [selectedValue, setSelectedValue] = useState('Empty :('); + + return ( + @@ -28,7 +44,7 @@ const CustomRadioGroup = ({ ...props }: any) => { }; return ( - + { storiesOf('RadioGroup', module) .add('default', () => ) .add('controlled', () => ( - + )) + .add('uncontrolled', () => ) .add('direction', () => ( <> diff --git a/packages/react-core/src/__tests__/__snapshots__/index.spec.ts.snap b/packages/react-core/src/__tests__/__snapshots__/index.spec.ts.snap index e05c4eb76b4..1cdc72f3a2f 100644 --- a/packages/react-core/src/__tests__/__snapshots__/index.spec.ts.snap +++ b/packages/react-core/src/__tests__/__snapshots__/index.spec.ts.snap @@ -7,5 +7,7 @@ Array [ "useAuthenticator", "useAuthenticatorInitMachine", "useAuthenticatorRoute", + "useHasValueUpdated", + "usePreviousValue", ] `; diff --git a/packages/react-core/src/index.ts b/packages/react-core/src/index.ts index ba41d6fff2d..79e8388b629 100644 --- a/packages/react-core/src/index.ts +++ b/packages/react-core/src/index.ts @@ -14,3 +14,6 @@ export { UseAuthenticatorRoute, FormFieldsComponent, } from './Authenticator'; + +// components/hooks/utils +export { useHasValueUpdated, usePreviousValue } from './hooks'; diff --git a/packages/react-native/src/primitives/Radio/Radio.tsx b/packages/react-native/src/primitives/Radio/Radio.tsx index 2f6f54e681a..40e7e94ff6c 100644 --- a/packages/react-native/src/primitives/Radio/Radio.tsx +++ b/packages/react-native/src/primitives/Radio/Radio.tsx @@ -66,7 +66,10 @@ export default function Radio({ style={[styles.radioContainer, radioContainerSize, radioContainerStyle]} > {selected ? ( - + ) : null} {label ? : null} diff --git a/packages/react-native/src/primitives/Radio/__tests__/__snapshots__/Radio.spec.tsx.snap b/packages/react-native/src/primitives/Radio/__tests__/__snapshots__/Radio.spec.tsx.snap index df39aea60ba..ddd0e5e0487 100644 --- a/packages/react-native/src/primitives/Radio/__tests__/__snapshots__/Radio.spec.tsx.snap +++ b/packages/react-native/src/primitives/Radio/__tests__/__snapshots__/Radio.spec.tsx.snap @@ -119,6 +119,7 @@ exports[`Radio renders as expected when passing a number to the size prop 1`] = undefined, ] } + testID="amplify__radio-button__dot" /> ({ children, direction = 'vertical', disabled, + initialValue, label, labelPosition = 'top', labelStyle, onChange, + onValueChange, size, style, - value, ...rest }: RadioGroupProps): JSX.Element { + const [value, setValue] = useState(initialValue); + + // track `hasValueUpdated` and `hasOnValueChangeUpdated`, + // only call `onValueChange` on `value` update + const hasValueUpdated = useHasValueUpdated(value); + const hasOnValueChangeUpdated = useHasValueUpdated(onValueChange); + + useEffect(() => { + if (hasValueUpdated) { + onValueChange?.(value); + } + }, [hasOnValueChangeUpdated, hasValueUpdated, onValueChange, value]); + const containerStyle: ViewStyle = useMemo( () => ({ flexDirection: getFlexDirectionFromLabelPosition(labelPosition), @@ -34,6 +58,15 @@ export default function RadioGroup({ [direction] ); + const handleChange = useCallback( + (nextValue: T | undefined) => { + setValue(nextValue); + + onChange?.(nextValue); + }, + [onChange] + ); + return ( @@ -50,7 +83,7 @@ export default function RadioGroup({ return cloneElement>(child, { disabled: isChildDisabled, - onChange, + onChange: handleChange, selected: isChildSelected, size: childSize ?? size, }); diff --git a/packages/react-native/src/primitives/RadioGroup/__tests__/RadioGroup.spec.tsx b/packages/react-native/src/primitives/RadioGroup/__tests__/RadioGroup.spec.tsx index 71becacf842..f7f1c9e5db1 100644 --- a/packages/react-native/src/primitives/RadioGroup/__tests__/RadioGroup.spec.tsx +++ b/packages/react-native/src/primitives/RadioGroup/__tests__/RadioGroup.spec.tsx @@ -30,7 +30,7 @@ const ControlledRadioGroup = ({ return ( { if (typeof nextValue === 'string') { handleOnChange(nextValue); @@ -94,4 +94,64 @@ describe('RadioGroup', () => { const { toJSON } = render(); expect(toJSON()).toMatchSnapshot(); }); + + it('updates the selected option in uncontrolled mode', () => { + const { getByTestId, queryByTestId } = render( + + + + + ); + + const selectedOption = queryByTestId('amplify__radio-button__dot'); + + expect(selectedOption).toBeNull(); + + const optionOne = getByTestId('option-1'); + + fireEvent.press(optionOne); + + expect(queryByTestId('amplify__radio-button__dot')).toBeDefined(); + }); + + it('only calls onValueChange when value changes', () => { + const onValueChange = jest.fn(); + + const { getByTestId, rerender } = render( + + + + + ); + + const optionOne = getByTestId('option-1'); + + fireEvent.press(optionOne); + + expect(onValueChange).toHaveBeenCalledTimes(1); + expect(onValueChange).toHaveBeenCalledWith('option-1'); + + const updatedOnValueChange = jest.fn(); + + rerender( + + + + + ); + + expect(updatedOnValueChange).not.toHaveBeenCalled(); + }); + + it('renders the label prop', () => { + const label = 'label'; + const { getByText } = render( + + + + + ); + + expect(getByText(label)).toBeDefined(); + }); }); diff --git a/packages/react-native/src/primitives/RadioGroup/__tests__/__snapshots__/RadioGroup.spec.tsx.snap b/packages/react-native/src/primitives/RadioGroup/__tests__/__snapshots__/RadioGroup.spec.tsx.snap index 00b47be81e5..2532162f201 100644 --- a/packages/react-native/src/primitives/RadioGroup/__tests__/__snapshots__/RadioGroup.spec.tsx.snap +++ b/packages/react-native/src/primitives/RadioGroup/__tests__/__snapshots__/RadioGroup.spec.tsx.snap @@ -75,6 +75,7 @@ exports[`RadioGroup renders as expected when direction is horizontal 1`] = ` undefined, ] } + testID="amplify__radio-button__dot" /> | 'labelStyle' | 'onChange' | 'size' - | 'value' >, ViewProps { children: React.ReactElement>[]; direction?: Direction; + initialValue?: T; + onValueChange?: (value?: T) => void; } export interface RadioGroupStyles {