diff --git a/example/src/navigation/navigator.tsx b/example/src/navigation/navigator.tsx
index b726e752..7b217144 100644
--- a/example/src/navigation/navigator.tsx
+++ b/example/src/navigation/navigator.tsx
@@ -6,6 +6,8 @@ import { Badges } from "../pages/Badges";
import { Icons } from "../pages/Icons";
import { Layout } from "../pages/Layout";
import { Logos } from "../pages/Logos";
+import { Selection } from "../pages/Selection";
+
import MainScreen from "../pages/MainScreen";
import { Pictograms } from "../pages/Pictograms";
import { Typography } from "../pages/Typography";
@@ -90,6 +92,14 @@ const AppNavigator = () => (
headerBackTitleVisible: false
}}
/>
+
);
diff --git a/example/src/pages/Selection.tsx b/example/src/pages/Selection.tsx
new file mode 100644
index 00000000..4db8bad1
--- /dev/null
+++ b/example/src/pages/Selection.tsx
@@ -0,0 +1,274 @@
+import {
+ CheckboxLabel,
+ ListItemCheckbox,
+ Divider,
+ H2,
+ NativeSwitch,
+ NewRadioItem,
+ RadioGroup,
+ SwitchLabel,
+ ListItemSwitch,
+ VSpacer
+} from "@pagopa/io-app-design-system";
+import React, { useState } from "react";
+import { View } from "react-native";
+import { ComponentViewerBox } from "../components/ComponentViewerBox";
+import { Screen } from "../components/Screen";
+
+export const Selection = () => (
+
+
+ Checkbox
+
+ {/* CheckboxLabel */}
+ {renderCheckboxLabel()}
+ {/* ListItemCheckbox */}
+ {renderListItemCheckbox()}
+
+ Radio
+
+ {/* RadioListItem */}
+
+
+ Switch
+
+ {/* Native Switch */}
+
+ {/* ListItemSwitch */}
+
+ {/* SwitchLabel */}
+ {renderAnimatedSwitch()}
+
+);
+
+const renderCheckboxLabel = () => (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+);
+
+const renderListItemCheckbox = () => (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+);
+
+// RADIO ITEMS
+
+const mockRadioItems = (): ReadonlyArray> => [
+ {
+ icon: "coggle",
+ value: "Let's try with a basic title",
+ description:
+ "Ti contatteranno solo i servizi che hanno qualcosa di importante da dirti. Potrai sempre disattivare le comunicazioni che non ti interessano.",
+ id: "example-1"
+ },
+ {
+ value: "Let's try with a basic title",
+ description:
+ "Ti contatteranno solo i servizi che hanno qualcosa di importante da dirti.",
+ id: "example-2"
+ },
+ {
+ value: "Let's try with a very looong loooooong title instead",
+ id: "example-3"
+ },
+ {
+ value: "Let's try with a disabled item",
+ description:
+ "Ti contatteranno solo i servizi che hanno qualcosa di importante da dirti.",
+ id: "example-disabled",
+ disabled: true
+ }
+];
+
+const RadioListItemsShowroom = () => {
+ const [selectedItem, setSelectedItem] = useState(
+ "example-1"
+ );
+
+ return (
+
+
+ key="check_income"
+ items={mockRadioItems()}
+ selectedItem={selectedItem}
+ onPress={setSelectedItem}
+ />
+
+ );
+};
+
+// SWITCH
+
+const renderAnimatedSwitch = () => (
+
+
+
+
+
+);
+
+const NativeSwitchShowroom = () => {
+ const [isEnabled, setIsEnabled] = useState(false);
+ const toggleSwitch = () => setIsEnabled(previousState => !previousState);
+
+ return (
+
+
+
+
+
+ );
+};
+
+type ListItemSwitchSampleProps = Pick<
+ React.ComponentProps,
+ "label" | "description" | "value"
+>;
+
+const ListItemSwitchSample = ({
+ value,
+ label,
+ description
+}: ListItemSwitchSampleProps) => {
+ const [isEnabled, setIsEnabled] = useState(value);
+ const toggleSwitch = () => setIsEnabled(previousState => !previousState);
+
+ return (
+
+ );
+};
+
+const ListItemSwitchShowroom = () => (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+);
diff --git a/src/components/buttons/ButtonLink.tsx b/src/components/buttons/ButtonLink.tsx
index 2e4eb731..7e131548 100644
--- a/src/components/buttons/ButtonLink.tsx
+++ b/src/components/buttons/ButtonLink.tsx
@@ -1,96 +1,98 @@
import React, { useCallback } from "react";
-import {
- GestureResponderEvent,
- Pressable,
- StyleSheet
-} from "react-native";
+import { GestureResponderEvent, Pressable, StyleSheet } from "react-native";
import Animated, {
- Extrapolate,
- interpolate,
- interpolateColor,
- useAnimatedProps,
- useAnimatedStyle,
- useDerivedValue,
- useSharedValue,
- withSpring
+ Extrapolate,
+ interpolate,
+ interpolateColor,
+ useAnimatedProps,
+ useAnimatedStyle,
+ useDerivedValue,
+ useSharedValue,
+ withSpring
} from "react-native-reanimated";
import { WithTestID } from "../../utils/types";
import { makeFontStyleObject } from "../../utils/fonts";
import { AnimatedIcon, IOIcons, IconClassComponent } from "../icons/Icon";
import { HSpacer } from "../spacer/Spacer";
-import { IOScaleValues, IOSpringValues, IOColors, IOButtonStyles } from "../../core";
+import {
+ IOScaleValues,
+ IOSpringValues,
+ IOColors,
+ IOButtonStyles
+} from "../../core";
type ColorButtonLink = "primary" | "error" | "warning" | "success" | "info";
export type ButtonLink = WithTestID<{
- color?: ColorButtonLink;
- label: string;
- disabled?: boolean;
- // Icons
- icon?: IOIcons;
- iconPosition?: "start" | "end";
- // Accessibility
- accessibilityLabel?: string;
- accessibilityHint?: string;
- // Events
- onPress: (event: GestureResponderEvent) => void;
+ color?: ColorButtonLink;
+ label: string;
+ disabled?: boolean;
+ // Icons
+ icon?: IOIcons;
+ iconPosition?: "start" | "end";
+ // Accessibility
+ accessibilityLabel?: string;
+ accessibilityHint?: string;
+ // Events
+ onPress: (event: GestureResponderEvent) => void;
}>;
type ColorStates = {
- label: {
- default: string;
- pressed: string;
- disabled: string;
- };
+ label: {
+ default: string;
+ pressed: string;
+ disabled: string;
+ };
};
const mapColorStates: Record, ColorStates> = {
- // Primary button
- primary: {
- label: {
- default: IOColors["blueIO-500"],
- pressed: IOColors["blueIO-600"],
- disabled: IOColors["grey-700"]
- }
- },
- error: {
- label: {
- default: IOColors["error-850"],
- pressed: IOColors["error-850"],
- disabled: IOColors["grey-700"]
- }
- },
- warning: {
- label: {
- default: IOColors["warning-850"],
- pressed: IOColors["warning-850"],
- disabled: IOColors["grey-700"]
- }
- },
- success: {
- label: {
- default: IOColors["success-850"],
- pressed: IOColors["success-850"],
- disabled: IOColors["grey-700"]
- }
- },
- info: {
- label: {
- default: IOColors["info-850"],
- pressed: IOColors["info-850"],
- disabled: IOColors["grey-700"]
- }
+ // Primary button
+ primary: {
+ label: {
+ default: IOColors["blueIO-500"],
+ pressed: IOColors["blueIO-600"],
+ disabled: IOColors["grey-700"]
}
+ },
+ error: {
+ label: {
+ default: IOColors["error-850"],
+ pressed: IOColors["error-850"],
+ disabled: IOColors["grey-700"]
+ }
+ },
+ warning: {
+ label: {
+ default: IOColors["warning-850"],
+ pressed: IOColors["warning-850"],
+ disabled: IOColors["grey-700"]
+ }
+ },
+ success: {
+ label: {
+ default: IOColors["success-850"],
+ pressed: IOColors["success-850"],
+ disabled: IOColors["grey-700"]
+ }
+ },
+ info: {
+ label: {
+ default: IOColors["info-850"],
+ pressed: IOColors["info-850"],
+ disabled: IOColors["grey-700"]
+ }
+ }
};
const DISABLED_OPACITY = 0.5;
const IOButtonStylesLocal = StyleSheet.create({
- label: {
- fontSize: 16,
- ...makeFontStyleObject("Regular", false, "ReadexPro")
- }
+ label: {
+ fontSize: 16,
+ ...makeFontStyleObject("Regular", false, "ReadexPro")
+ }
});
-export const ButtonLink = React.memo(({
+export const ButtonLink = React.memo(
+ ({
color = "primary",
label,
disabled = false,
@@ -100,7 +102,7 @@ export const ButtonLink = React.memo(({
accessibilityLabel,
accessibilityHint,
testID
-}: ButtonLink) => {
+ }: ButtonLink) => {
const isPressed = useSharedValue(0);
// Scaling transformation applied when the button is pressed
@@ -108,138 +110,136 @@ export const ButtonLink = React.memo(({
// Using a spring-based animation for our interpolations
const progressPressed = useDerivedValue(() =>
- withSpring(isPressed.value, IOSpringValues.button)
+ withSpring(isPressed.value, IOSpringValues.button)
);
// Interpolate animation values from `isPressed` values
const pressedAnimationStyle = useAnimatedStyle(() => {
- // Link color states to the pressed states
-
- // Scale down button slightly when pressed
- const scale = interpolate(
- progressPressed.value,
- [0, 1],
- [1, animationScaleValue],
- Extrapolate.CLAMP
- );
-
- return {
- transform: [{ scale }]
- };
+ // Link color states to the pressed states
+
+ // Scale down button slightly when pressed
+ const scale = interpolate(
+ progressPressed.value,
+ [0, 1],
+ [1, animationScaleValue],
+ Extrapolate.CLAMP
+ );
+
+ return {
+ transform: [{ scale }]
+ };
});
const pressedColorLabelAnimationStyle = useAnimatedStyle(() => {
- // Link color states to the pressed states
-
- /* ◀ REMOVE_LEGACY_COMPONENT: Remove the following condition */
- const labelColor = interpolateColor(
- progressPressed.value,
- [0, 1],
- [
- mapColorStates[color].label.default,
- mapColorStates[color].label.pressed
- ]
- );
-
- return {
- color: labelColor
- };
+ // Link color states to the pressed states
+
+ const labelColor = interpolateColor(
+ progressPressed.value,
+ [0, 1],
+ [
+ mapColorStates[color].label.default,
+ mapColorStates[color].label.pressed
+ ]
+ );
+
+ return {
+ color: labelColor
+ };
});
// Animate the color prop
const pressedColorIconAnimationStyle = useAnimatedProps(() => {
- const iconColor = interpolateColor(
- progressPressed.value,
- [0, 1],
- [
- mapColorStates[color].label.default,
- mapColorStates[color].label.pressed
- ]
- );
-
- return { color: iconColor };
+ const iconColor = interpolateColor(
+ progressPressed.value,
+ [0, 1],
+ [
+ mapColorStates[color].label.default,
+ mapColorStates[color].label.pressed
+ ]
+ );
+
+ return { color: iconColor };
});
const AnimatedIconClassComponent =
- Animated.createAnimatedComponent(IconClassComponent);
+ Animated.createAnimatedComponent(IconClassComponent);
const onPressIn = useCallback(() => {
- // eslint-disable-next-line functional/immutable-data
- isPressed.value = 1;
+ // eslint-disable-next-line functional/immutable-data
+ isPressed.value = 1;
}, [isPressed]);
const onPressOut = useCallback(() => {
- // eslint-disable-next-line functional/immutable-data
- isPressed.value = 0;
+ // eslint-disable-next-line functional/immutable-data
+ isPressed.value = 0;
}, [isPressed]);
// Icon size
const iconSize = 24;
return (
-
-
+
- {icon && (
- <>
- {!disabled ? (
-
- ) : (
-
- )}
-
- >
- )}
-
+ {icon && (
+ <>
+ {!disabled ? (
+
+ ) : (
+
+ )}
+
+ >
+ )}
+
- {label}
-
-
-
+ >
+ {label}
+
+
+
);
-});
+ }
+);
export default ButtonLink;
diff --git a/src/components/buttons/IconButton.tsx b/src/components/buttons/IconButton.tsx
index 1e022bce..ccb1b301 100644
--- a/src/components/buttons/IconButton.tsx
+++ b/src/components/buttons/IconButton.tsx
@@ -10,7 +10,14 @@ import Animated, {
useSharedValue,
withSpring
} from "react-native-reanimated";
-import { IOButtonStyles, IOColors, IOIconButtonStyles, IOScaleValues, IOSpringValues, hexToRgba } from "../../core";
+import {
+ IOButtonStyles,
+ IOColors,
+ IOIconButtonStyles,
+ IOScaleValues,
+ IOSpringValues,
+ hexToRgba
+} from "../../core";
import { WithTestID } from "../../utils/types";
import { AnimatedIcon, IOIcons, IconClassComponent } from "../icons";
@@ -101,14 +108,10 @@ export const IconButton = ({
const iconColor = interpolateColor(
progressPressed.value,
[0, 1],
- [
- mapColorStates[color].icon.default,
- mapColorStates[color].icon.pressed
- ]
+ [mapColorStates[color].icon.default, mapColorStates[color].icon.pressed]
);
return { color: iconColor };
});
- /* REMOVE_LEGACY_COMPONENT: End ▶ */
const handlePressIn = useCallback(() => {
// eslint-disable-next-line functional/immutable-data
diff --git a/src/components/checkbox/AnimatedCheckbox.tsx b/src/components/checkbox/AnimatedCheckbox.tsx
index 6db6ead4..26a878e5 100644
--- a/src/components/checkbox/AnimatedCheckbox.tsx
+++ b/src/components/checkbox/AnimatedCheckbox.tsx
@@ -85,7 +85,6 @@ export const AnimatedCheckbox = ({ checked, onPress, disabled }: OwnProps) => {
onPress={onPress}
style={styles.checkBoxWrapper}
>
- {/* ◀ REMOVE_LEGACY_COMPONENT: Remove the following conditions */}
{
animatedCheckboxSquare
]}
/>
- {/* REMOVE_LEGACY_COMPONENT: End ▶ */}
{isChecked && (
void;
+};
+
+const DISABLED_OPACITY = 0.5;
+
+// disabled: the component is no longer touchable
+// onPress:
+type OwnProps = Props &
+ Pick<
+ React.ComponentProps,
+ "onPress" | "accessibilityLabel" | "disabled"
+ >;
+
+/**
+ * with the automatic state management that uses a {@link AnimatedCheckBox}
+ * The toggleValue change when a `onPress` event is received and dispatch the `onValueChange`.
+ *
+ * @param props
+ * @constructor
+ */
+export const ListItemCheckbox = ({
+ value,
+ description,
+ icon,
+ selected,
+ disabled,
+ onValueChange
+}: OwnProps) => {
+ const [toggleValue, setToggleValue] = useState(selected ?? false);
+ // Animations
+ const isPressed: Animated.SharedValue = useSharedValue(0);
+
+ // Scaling transformation applied when the button is pressed
+ const animationScaleValue = IOScaleValues?.basicButton?.pressedState;
+
+ const progressPressed = useDerivedValue(() =>
+ withSpring(isPressed.value, IOSpringValues.button)
+ );
+
+ // Theme
+ const theme = useIOTheme();
+
+ const mapBackgroundStates: Record = {
+ default: hexToRgba(IOColors[theme["listItem-pressed"]], 0),
+ pressed: IOColors[theme["listItem-pressed"]]
+ };
+
+ // Interpolate animation values from `isPressed` values
+ const animatedScaleStyle = useAnimatedStyle(() => {
+ const scale = interpolate(
+ progressPressed.value,
+ [0, 1],
+ [1, animationScaleValue],
+ Extrapolate.CLAMP
+ );
+
+ return {
+ transform: [{ scale }]
+ };
+ });
+
+ const handlePressIn = useCallback(() => {
+ // eslint-disable-next-line functional/immutable-data
+ isPressed.value = 1;
+ }, [isPressed]);
+ const handlePressOut = useCallback(() => {
+ // eslint-disable-next-line functional/immutable-data
+ isPressed.value = 0;
+ }, [isPressed]);
+
+ const animatedBackgroundStyle = useAnimatedStyle(() => {
+ const backgroundColor = interpolateColor(
+ progressPressed.value,
+ [0, 1],
+ [mapBackgroundStates.default, mapBackgroundStates.pressed]
+ );
+
+ return {
+ backgroundColor
+ };
+ });
+
+ const toggleCheckbox = () => {
+ ReactNativeHapticFeedback.trigger("impactLight");
+ setToggleValue(!toggleValue);
+ if (onValueChange !== undefined) {
+ onValueChange(!toggleValue);
+ }
+ };
+
+ return (
+
+
+
+
+
+ {icon && (
+
+
+
+ )}
+
+ {value}
+
+
+
+
+
+
+
+ {description && (
+
+
+
+ {description}
+
+
+ )}
+
+
+
+ );
+};
diff --git a/src/components/listitems/ListItemRadio.tsx b/src/components/listitems/ListItemRadio.tsx
new file mode 100644
index 00000000..ae5b89bd
--- /dev/null
+++ b/src/components/listitems/ListItemRadio.tsx
@@ -0,0 +1,185 @@
+import * as React from "react";
+import { useCallback, useState } from "react";
+import { Pressable, View } from "react-native";
+import ReactNativeHapticFeedback from "react-native-haptic-feedback";
+import Animated, {
+ Extrapolate,
+ interpolate,
+ interpolateColor,
+ useAnimatedStyle,
+ useDerivedValue,
+ useSharedValue,
+ withSpring
+} from "react-native-reanimated";
+import {
+ IOColors,
+ IOScaleValues,
+ IOSelectionListItemStyles,
+ IOSelectionListItemVisualParams,
+ IOSpringValues,
+ IOStyles,
+ hexToRgba,
+ useIOTheme
+} from "../../core";
+import { WithTestID } from "../../utils/types";
+import { IOIcons, Icon } from "../icons";
+import { HSpacer, VSpacer } from "../spacer";
+import { H6, LabelSmall } from "../typography";
+import { AnimatedRadio } from "../radio/AnimatedRadio";
+
+type Props = WithTestID<{
+ value: string;
+ description?: string;
+ icon?: IOIcons;
+ selected: boolean;
+ onValueChange?: (newValue: boolean) => void;
+}>;
+
+const DISABLED_OPACITY = 0.5;
+
+// disabled: the component is no longer touchable
+// onPress:
+type OwnProps = Props &
+ Pick<
+ React.ComponentProps,
+ "onPress" | "accessibilityLabel" | "disabled"
+ >;
+
+/**
+ * with the automatic state management that uses a {@link AnimatedCheckBox}
+ * The toggleValue change when a `onPress` event is received and dispatch the `onValueChange`.
+ *
+ * @param props
+ * @constructor
+ */
+export const ListItemRadio = ({
+ value,
+ description,
+ icon,
+ selected,
+ disabled,
+ onValueChange,
+ testID
+}: OwnProps) => {
+ const [toggleValue, setToggleValue] = useState(selected ?? false);
+ // Animations
+ const isPressed: Animated.SharedValue = useSharedValue(0);
+
+ // Scaling transformation applied when the button is pressed
+ const animationScaleValue = IOScaleValues?.basicButton?.pressedState;
+
+ const progressPressed = useDerivedValue(() =>
+ withSpring(isPressed.value, IOSpringValues.button)
+ );
+
+ // Theme
+ const theme = useIOTheme();
+
+ const mapBackgroundStates: Record = {
+ default: hexToRgba(IOColors[theme["listItem-pressed"]], 0),
+ pressed: IOColors[theme["listItem-pressed"]]
+ };
+
+ // Interpolate animation values from `isPressed` values
+ const animatedScaleStyle = useAnimatedStyle(() => {
+ const scale = interpolate(
+ progressPressed.value,
+ [0, 1],
+ [1, animationScaleValue],
+ Extrapolate.CLAMP
+ );
+
+ return {
+ transform: [{ scale }]
+ };
+ });
+
+ const handlePressIn = useCallback(() => {
+ // eslint-disable-next-line functional/immutable-data
+ isPressed.value = 1;
+ }, [isPressed]);
+ const handlePressOut = useCallback(() => {
+ // eslint-disable-next-line functional/immutable-data
+ isPressed.value = 0;
+ }, [isPressed]);
+
+ const animatedBackgroundStyle = useAnimatedStyle(() => {
+ const backgroundColor = interpolateColor(
+ progressPressed.value,
+ [0, 1],
+ [mapBackgroundStates.default, mapBackgroundStates.pressed]
+ );
+
+ return {
+ backgroundColor
+ };
+ });
+
+ const toggleRadioItem = () => {
+ ReactNativeHapticFeedback.trigger("impactLight");
+ setToggleValue(!toggleValue);
+ if (onValueChange !== undefined) {
+ onValueChange(!toggleValue);
+ }
+ };
+
+ return (
+
+
+
+
+
+ {icon && (
+
+
+
+ )}
+
+
+ {value}
+
+
+
+
+
+
+
+ {description && (
+
+
+
+ {description}
+
+
+ )}
+
+
+
+ );
+};
diff --git a/src/components/listitems/ListItemSwitch.tsx b/src/components/listitems/ListItemSwitch.tsx
new file mode 100644
index 00000000..bf152953
--- /dev/null
+++ b/src/components/listitems/ListItemSwitch.tsx
@@ -0,0 +1,94 @@
+import React from "react";
+import { Switch, View } from "react-native";
+import {
+ IOSelectionListItemStyles,
+ IOSelectionListItemVisualParams,
+ IOStyles,
+ useIOTheme
+} from "../../core";
+import { IOIcons, Icon } from "../icons";
+import { HSpacer, VSpacer } from "../spacer";
+import { H6, LabelSmall } from "../typography";
+import { NativeSwitch } from "../switch/NativeSwitch";
+
+type Props = {
+ label: string;
+ onSwitchValueChange?: (newValue: boolean) => void;
+ description?: string;
+ icon?: IOIcons;
+};
+
+const DISABLED_OPACITY = 0.5;
+
+type OwnProps = Props &
+ Pick, "value" | "disabled">;
+
+export const ListItemSwitch = React.memo(
+ ({
+ label,
+ description,
+ icon,
+ value,
+ disabled,
+ onSwitchValueChange
+ }: OwnProps) => {
+ const theme = useIOTheme();
+
+ return (
+
+
+
+
+ {icon && (
+
+
+
+ )}
+
+ {label}
+
+
+ {description && (
+ <>
+
+
+ {description}
+
+ >
+ )}
+
+
+
+
+
+
+
+
+ );
+ }
+);
diff --git a/src/components/listitems/index.tsx b/src/components/listitems/index.tsx
index 555bdc96..4c809073 100644
--- a/src/components/listitems/index.tsx
+++ b/src/components/listitems/index.tsx
@@ -6,3 +6,6 @@ export * from "./ListItemNav";
export * from "./ListItemNavAlert";
export * from "./ListItemTransaction";
export * from "./PressableListItemsBase";
+export * from "./ListItemSwitch";
+export * from "./ListItemCheckbox";
+export * from "./ListItemRadio";
diff --git a/src/components/radio/AnimatedRadio.tsx b/src/components/radio/AnimatedRadio.tsx
index 6b06cac9..f471f306 100644
--- a/src/components/radio/AnimatedRadio.tsx
+++ b/src/components/radio/AnimatedRadio.tsx
@@ -83,7 +83,6 @@ export const AnimatedRadio = ({ checked, onPress, disabled }: OwnProps) => {
onPress={onPress}
style={styles.radioWrapper}
>
- {/* ◀ REMOVE_LEGACY_COMPONENT: Remove the following conditions */}
{
animatedCheckboxSquare
]}
/>
- {/* REMOVE_LEGACY_COMPONENT: End ▶ */}
{isChecked && (
= {
+ id: T;
+ value: string;
+ description?: string;
+ icon?: IOIcons;
+ disabled?: boolean;
+};
+
+type Props = {
+ items: ReadonlyArray>;
+ selectedItem?: T;
+ onPress: (selected: T) => void;
+};
+
+/**
+ * A list of radio buttons.
+ * The management of the selection is demanded and derived by the `selectedItem` prop.
+ * The item with the `id` equal to the `selectedItem` is the active one.
+ */
+export const RadioGroup = ({ items, selectedItem, onPress }: Props) => (
+
+ {items.map((item, index) => (
+
+ onPress(item.id)}
+ selected={selectedItem === item.id}
+ />
+ {index < items.length - 1 && }
+
+ ))}
+
+);
diff --git a/src/components/radio/index.tsx b/src/components/radio/index.tsx
index e308948e..8b703f05 100644
--- a/src/components/radio/index.tsx
+++ b/src/components/radio/index.tsx
@@ -1 +1,2 @@
export * from "./AnimatedRadio";
+export * from "./RadioGroup";