diff --git a/docs/docs/components/radioButton.md b/docs/docs/components/radioButton.md
new file mode 100644
index 0000000..a2c921c
--- /dev/null
+++ b/docs/docs/components/radioButton.md
@@ -0,0 +1,121 @@
+import {RadioButton, HStack} from '@amalgama/react-native-ui-kit'
+import {UIKitIcon} from '@amalgama/react-native-ui-kit/'
+import CodePreview from '@site/src/components/CodePreview'
+import ExampleRadioButton from '@site/src/components/ExampleRadioButton'
+import {useState} from 'react'
+
+# RadioButton
+
+## Import
+
+To add the `RadioButton` component to your project you can import it as follows:
+
+```tsx
+import {RadioButton} from '@amalgama/react-native-ui-kit';
+```
+
+## Examples
+
+
+
+
+## Props
+
+### as
+The icon components family to use. See [Icon - as prop documentation](./icon.md#as) for more information.
+The default value is `UIKitIcon`.
+
+```jsx
+import UIKitIcon from '@amalgama/react-native-ui-kit';
+
+ { window && window.alert( 'Clicked!' );}}
+/>
+```
+
+### selectedIcon
+The name of the icon from the icon family selected to use when the button is selected. See [Icon - name prop documentation](./icon.md#name) for more information. The default value is `circle-filled`.
+
+```jsx
+import UIKitIcon from '@amalgama/react-native-ui-kit';
+
+ { window && window.alert( 'Clicked!' );}}
+/>
+```
+
+### unselectedIcon
+
+The name of the icon from the icon family selected to use when the button is unselected. See [Icon - name prop documentation](./icon.md#name) for more information. The default value is `circle`.
+
+```jsx
+import UIKitIcon from '@amalgama/react-native-ui-kit';
+
+ { window && window.alert( 'Clicked!' );}}
+/>
+```
+### selected
+If the RadioButton is selected or not.
+
+| TYPE | REQUIRED | DEFAULT |
+| ---- | -------- | ------- |
+| bool | No | false |
+
+
+
+
+
+
+
+
+### disabled
+If the radioButton is disabled or not.
+
+| TYPE | REQUIRED | DEFAULT |
+| ---- | -------- | ------- |
+| bool | No | false |
+
+
+
+
+
+
+
+
+
+### onPress
+Invoked when the RadioButton is pressed.
+
+| TYPE | REQUIRED |
+| -------- | -------- |
+| function | No |
+
+
+ { window.alert( 'The RadioButton was pressed!' ) } }/>
+
+
+```jsx
+ { window.alert( 'The radioButton was pressed!' ) } }/>
+```
+
+## Accessibility props
+[React Native accessibility docs](https://reactnative.dev/docs/accessibility)
+
+### accessible
+Sets the component to an accessibility element. It is set by default to `true`.
+
+### accessibilityRole
+Communicates the purpose of the component. It is set by default to `"radio"`.
+
+### accessibilityState
+Describes the current state of the element. By default, indicates if the `RadioButton` is `disabled`, `selected` or `unselected`.
\ No newline at end of file
diff --git a/docs/src/components/ExampleRadioButton/index.tsx b/docs/src/components/ExampleRadioButton/index.tsx
new file mode 100644
index 0000000..5d14295
--- /dev/null
+++ b/docs/src/components/ExampleRadioButton/index.tsx
@@ -0,0 +1,14 @@
+import { RadioButton } from '@amalgama/react-native-ui-kit';
+import React, { useState } from 'react';
+
+const ExampleRadioButton = () => {
+ const [ isSelected, setSelected ] = useState( false );
+ return (
+ { setSelected( prev => !prev ); } }
+ />
+ );
+};
+
+export default ExampleRadioButton;
diff --git a/example/src/App.tsx b/example/src/App.tsx
index bdd8e45..b607fa5 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -5,6 +5,7 @@ import { ThemeProvider, VStack, extendThemeConfig } from '@amalgama/react-native
import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome';
import IoniconsIcon from 'react-native-vector-icons/Ionicons';
+import RadioButtonExamples from './components/RadioButtonExamples';
import TextExamples from './components/TextExamples';
import BoxExamples from './components/BoxExamples';
import ButtonExamples from './components/ButtonExamples';
@@ -87,6 +88,7 @@ export default function App() {
+
diff --git a/example/src/components/RadioButtonExamples.tsx b/example/src/components/RadioButtonExamples.tsx
new file mode 100644
index 0000000..79a0345
--- /dev/null
+++ b/example/src/components/RadioButtonExamples.tsx
@@ -0,0 +1,64 @@
+/* eslint-disable no-alert */
+import * as React from 'react';
+
+import { StyleSheet, View } from 'react-native';
+import {
+ RadioButton, VStack, Text, HStack
+} from '@amalgama/react-native-ui-kit';
+
+const styles = StyleSheet.create( {
+ container: {
+ width: '100%',
+ marginBottom: 20
+ },
+ separator: {
+ height: 1,
+ minWidth: '100%',
+ marginTop: 2,
+ marginBottom: 6,
+ backgroundColor: 'black'
+ },
+ vspace: {
+ height: 10,
+ minWidth: '100%'
+ }
+} );
+
+const RadioButtonExamples = () => {
+ const [ selected, setSelected ] = React.useState( false );
+ return (
+
+ RadioButton Component
+
+ Enabled
+
+
+
+
+
+
+
+
+ Disabled
+
+
+
+
+
+
+
+
+ On press
+
+
+
+ setSelected( prev => !prev )} />
+
+
+
+
+
+ );
+};
+
+export default RadioButtonExamples;
diff --git a/src/components/hooks/index.tsx b/src/components/hooks/index.tsx
new file mode 100644
index 0000000..6933a30
--- /dev/null
+++ b/src/components/hooks/index.tsx
@@ -0,0 +1,3 @@
+export { default as useIsFocused } from './useIsFocused';
+export { default as useIsHovered } from './useIsHovered';
+export { default as useIsPressed } from './useIsPressed';
diff --git a/src/components/index.ts b/src/components/index.ts
index 60408fe..d0fb58b 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -6,3 +6,4 @@ export { default as Text } from './main/Text';
export { default as Icon } from './main/Icon';
export { default as IconButton } from './main/IconButton';
export { default as Checkbox } from './main/Checkbox';
+export { default as RadioButton } from './main/RadioButton';
diff --git a/src/components/main/IconButton/index.tsx b/src/components/main/IconButton/index.tsx
index 68d6a62..a87b651 100644
--- a/src/components/main/IconButton/index.tsx
+++ b/src/components/main/IconButton/index.tsx
@@ -1,16 +1,17 @@
-import React from 'react';
+import React, { memo, forwardRef } from 'react';
import Pressable from '../Pressable';
import Icon from '../Icon';
import { useIconButtonPropsResolver } from './hooks';
import type { IIconButtonProps } from './types';
-const IconButton = ( { name, ...props }: IIconButtonProps ) => {
+const IconButton = ( { name, ...props }: IIconButtonProps, ref?:any ) => {
const {
iconProps, containerProps
} = useIconButtonPropsResolver( props );
return (
{
);
};
-export default IconButton;
+export default memo( forwardRef( IconButton ) );
diff --git a/src/components/main/RadioButton/hooks.ts b/src/components/main/RadioButton/hooks.ts
new file mode 100644
index 0000000..236ec50
--- /dev/null
+++ b/src/components/main/RadioButton/hooks.ts
@@ -0,0 +1,54 @@
+import { useMemo } from 'react';
+import { useComponentPropsResolver } from '../../../hooks';
+import type { IRadioButtonProps } from './types';
+import { useIsFocused, useIsHovered, useIsPressed } from '../../hooks';
+import type { IIconButtonProps } from '../IconButton/types';
+
+interface IUseRadioButtonPropsResolverReturnType {
+ iconProps: IRadioButtonProps[ '__icon' ];
+ restProps: Omit;
+}
+
+const useRadioButtonPropsResolver = (
+ {
+ selectedIcon,
+ unselectedIcon,
+ ...props
+ } : IRadioButtonProps ) : IUseRadioButtonPropsResolverReturnType => {
+ const { isPressed, onPressIn, onPressOut } = useIsPressed( props );
+ const { isHovered, onHoverIn, onHoverOut } = useIsHovered( props );
+ const { isFocused, onFocus, onBlur } = useIsFocused( props );
+
+ const { selected, disabled, testID } = props;
+ const state = useMemo( () => ( {
+ isDisabled: !!disabled,
+ isSelected: !!selected,
+ isPressed,
+ isHovered,
+ isFocused
+ } ), [ disabled, selected, isFocused, isHovered, isPressed ] );
+
+ const {
+ __icon: iconProps,
+ ...restProps
+ } = useComponentPropsResolver( 'RadioButton', props, state ) as IIconButtonProps;
+ const name = ( props.selected ? selectedIcon : unselectedIcon ) as string;
+
+ const iconPropsWithTestId = { ...iconProps, testID: !!testID && `${testID}-icon` };
+
+ return {
+ iconProps: iconPropsWithTestId,
+ restProps: {
+ ...restProps,
+ name,
+ onPressIn,
+ onPressOut,
+ onHoverIn,
+ onHoverOut,
+ onFocus,
+ onBlur
+ }
+ };
+};
+
+export default useRadioButtonPropsResolver;
diff --git a/src/components/main/RadioButton/index.tsx b/src/components/main/RadioButton/index.tsx
new file mode 100644
index 0000000..9d1538a
--- /dev/null
+++ b/src/components/main/RadioButton/index.tsx
@@ -0,0 +1,38 @@
+import React, { forwardRef, memo } from 'react';
+import UIKitIcon from '../../../icons/UIKitIcon';
+import IconButton from '../IconButton';
+import useRadioButtonPropsResolver from './hooks';
+import type { IRadioButtonProps } from './types';
+
+const RadioButton = ( {
+ as = UIKitIcon,
+ selectedIcon = 'circle-filled',
+ unselectedIcon = 'circle',
+ selected = false,
+ ...props
+}: IRadioButtonProps, ref?: any ) => {
+ const { iconProps, restProps } = useRadioButtonPropsResolver( {
+ ...props,
+ selectedIcon,
+ unselectedIcon,
+ selected
+ } );
+
+ return (
+
+ );
+};
+
+export default memo( forwardRef( RadioButton ) );
diff --git a/src/components/main/RadioButton/types.ts b/src/components/main/RadioButton/types.ts
new file mode 100644
index 0000000..fc2d8a5
--- /dev/null
+++ b/src/components/main/RadioButton/types.ts
@@ -0,0 +1,11 @@
+import type { ComponentStyledProps } from '../../../core/components/types';
+import type { IPressableProps } from '../Pressable/types';
+import type { IIconProps } from '../Icon/types';
+
+export interface IRadioButtonProps extends Omit,
+ComponentStyledProps<'RadioButton'>{
+ selectedIcon?: string;
+ unselectedIcon?: string;
+ selected?: boolean;
+ size?: IIconProps['size'];
+}
diff --git a/src/core/components/types/common.ts b/src/core/components/types/common.ts
index 1e5e985..3d27a1b 100644
--- a/src/core/components/types/common.ts
+++ b/src/core/components/types/common.ts
@@ -1,6 +1,7 @@
import type { StyledProps } from '../../theme/types';
-export type ComponentName = 'Text' | 'Box' | 'Stack' | 'Button' | 'Pressable' | 'Icon' | 'IconButton' | 'Checkbox';
+export type ComponentName = 'Text' | 'Box' | 'Stack' | 'Button' | 'Pressable' | 'Icon' | 'Checkbox'
+| 'IconButton' | 'RadioButton';
export type VariantName = string;
diff --git a/src/core/components/types/customProps.ts b/src/core/components/types/customProps.ts
index 4ff23a5..a7bace9 100644
--- a/src/core/components/types/customProps.ts
+++ b/src/core/components/types/customProps.ts
@@ -15,6 +15,10 @@ interface IconCustomProps {
interface IconButtonCustomProps {
as?: any
}
+// RadioButton
+interface RadioButtonCustomProps {
+ as?: any
+}
// Stack
interface StackCustomProps {
@@ -27,6 +31,7 @@ interface StackCustomProps {
interface ComponentsCustomPropsConfig {
Icon: IconCustomProps,
IconButton: IconButtonCustomProps,
+ RadioButton: RadioButtonCustomProps,
Stack: StackCustomProps
}
diff --git a/src/core/components/types/pseudoProps.ts b/src/core/components/types/pseudoProps.ts
index fa1e275..4044402 100644
--- a/src/core/components/types/pseudoProps.ts
+++ b/src/core/components/types/pseudoProps.ts
@@ -38,8 +38,9 @@ interface ICheckboxPseudoProps {
// Pseudoprops config for all components
interface ComponentsPseudoPropsConfig {
Button: IButtonPseudoProps,
- IconButton: IIconButtonPseudoProps
- Checkbox: ICheckboxPseudoProps
+ Checkbox: ICheckboxPseudoProps,
+ IconButton: IIconButtonPseudoProps,
+ RadioButton: IIconButtonPseudoProps
}
// Template type to get pseudoprops for a given component
diff --git a/src/core/components/types/state.ts b/src/core/components/types/state.ts
index dd5fcac..cf13793 100644
--- a/src/core/components/types/state.ts
+++ b/src/core/components/types/state.ts
@@ -26,8 +26,8 @@ import type { ValueOf } from '../../types';
export const COMPONENT_STATE_PROPS_MAP = {
'isPressed': '__pressed',
- 'isDisabled': '__disabled',
'isSelected': '__selected',
+ 'isDisabled': '__disabled',
'isIndeterminated': '__indeterminated',
'isHovered': '__hovered',
'isFocused': '__focused'
diff --git a/src/core/theme/defaultTheme/components/RadioButton.ts b/src/core/theme/defaultTheme/components/RadioButton.ts
new file mode 100644
index 0000000..cf98bb2
--- /dev/null
+++ b/src/core/theme/defaultTheme/components/RadioButton.ts
@@ -0,0 +1,36 @@
+export default {
+ defaultProps: {
+ height: 32,
+ width: 32,
+ justifyContent: 'center',
+ alignItems: 'center',
+ rounded: 'full',
+ outlineStyle: 'none',
+ __icon: {
+ color: 'neutral.800',
+ size: 'sm'
+ },
+ __disabled: {
+ __icon: {
+ color: 'neutral.600'
+ }
+ },
+ __selected: {
+ __icon: {
+ color: 'secondary.900'
+ }
+ },
+ __pressed: {
+ backgroundColor: 'secondary.50'
+ },
+ __hovered: {
+ backgroundColor: 'secondary.50'
+ },
+ __focused: {
+ borderWidth: '1',
+ backgroundColor: 'secondary.50',
+ borderColor: 'secondary.900'
+ }
+
+ }
+};
diff --git a/src/core/theme/defaultTheme/components/index.ts b/src/core/theme/defaultTheme/components/index.ts
index da575d9..9457a5c 100644
--- a/src/core/theme/defaultTheme/components/index.ts
+++ b/src/core/theme/defaultTheme/components/index.ts
@@ -4,6 +4,7 @@ import Icon from './Icon';
import IconButton from './IconButton';
import Stack from './Stack';
import Checkbox from './Checkbox';
+import RadioButton from './RadioButton';
export default {
Text,
@@ -11,5 +12,6 @@ export default {
IconButton,
Button,
Stack,
- Checkbox
+ Checkbox,
+ RadioButton
};
diff --git a/src/core/theme/defaultTheme/palette.ts b/src/core/theme/defaultTheme/palette.ts
index bca4e37..98ae2af 100644
--- a/src/core/theme/defaultTheme/palette.ts
+++ b/src/core/theme/defaultTheme/palette.ts
@@ -14,6 +14,7 @@ export default {
600: '#95B3FF',
400: '#B9CCFF',
200: '#DCE6FF',
+ 50: '#EDF2FF',
10: '#EDF2FF'
},
neutral: {
diff --git a/src/index.tsx b/src/index.tsx
index 0083be3..eb92aa0 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,3 +1,5 @@
+import UIKitIcon from './icons/UIKitIcon';
+
export { default as Theme } from './core/theme/Theme';
export { default as defaultTheme } from './core/theme/defaultTheme';
export { default as extendThemeConfig } from './core/theme/extendThemeConfig';
@@ -11,5 +13,7 @@ export {
IconButton,
VStack,
Text,
- Checkbox
+ Checkbox,
+ RadioButton
} from './components';
+export { UIKitIcon };
diff --git a/tests/components/main/RadioButton.test.js b/tests/components/main/RadioButton.test.js
new file mode 100644
index 0000000..17a2f0f
--- /dev/null
+++ b/tests/components/main/RadioButton.test.js
@@ -0,0 +1,95 @@
+import { fireEvent, render } from '@testing-library/react-native';
+import React from 'react';
+import {
+ RadioButton, ThemeProvider
+} from '../../../src';
+
+const TEST_ID = 'test-radio-button';
+
+const accessibilityTest = ( {
+ getByTestId,
+ testID = TEST_ID,
+ checked = false,
+ disabled = false
+} ) => {
+ expect( getByTestId( testID ) ).toHaveProp( 'accessible', true );
+ expect( getByTestId( testID ) ).toHaveProp( 'accessibilityRole', 'radio' );
+ expect( getByTestId( testID ) ).toHaveProp( 'accessibilityState', { checked, disabled } );
+};
+
+const hasChildWithProp = ( elem, prop, value ) => {
+ let hasProperty = false;
+ for ( let index = 0; index < elem.children.length && !hasProperty; index += 1 ) {
+ const element = elem.children[ index ];
+ if ( element.props[ prop ] === value || hasChildWithProp( element, prop, value ) ) {
+ hasProperty = true;
+ }
+ }
+ return hasProperty;
+};
+
+describe( 'RadioButton', () => {
+ const renderRadioButton = ( { selected = false, onPress, disabled } = {} ) => render(
+
+
+
+ );
+
+ test( 'renders the unselected style when unselected', () => {
+ const { getByTestId } = renderRadioButton();
+ const icon = getByTestId( `${TEST_ID}-icon` );
+ expect( hasChildWithProp( icon, 'stroke', '#676A79' ) ).toBe( true );
+ accessibilityTest( { getByTestId } );
+ } );
+
+ test( 'renders the selected style when selected', () => {
+ const { getByTestId } = renderRadioButton( { selected: true } );
+ const icon = getByTestId( `${TEST_ID}-icon` );
+ expect( hasChildWithProp( icon, 'stroke', '#4F80FF' ) ).toBe( true );
+ accessibilityTest( { getByTestId, checked: true } );
+ } );
+
+ describe( 'when onPress function is provided', () => {
+ test( 'calls onPress function when pressed', () => {
+ const onPress = jest.fn();
+ const { getByTestId } = renderRadioButton( { onPress } );
+ fireEvent.press( getByTestId( TEST_ID ) );
+ expect( onPress ).toHaveBeenCalled();
+ } );
+ } );
+
+ describe( 'when the radio button is disabled', () => {
+ describe( 'when pressed', () => {
+ test( 'does not calls onPress', () => {
+ const onPress = jest.fn();
+ const { queryByTestId } = renderRadioButton( { disabled: true, onPress } );
+ fireEvent.press( queryByTestId( TEST_ID ) );
+ expect( onPress ).not.toHaveBeenCalled();
+ } );
+ } );
+ describe( 'when is unselected', () => {
+ it( 'should render the disabled style', () => {
+ const { getByTestId } = renderRadioButton( { disabled: true } );
+ const icon = getByTestId( `${TEST_ID}-icon` );
+ expect( hasChildWithProp( icon, 'stroke', '#B0B4CD' ) ).toBe( true );
+ accessibilityTest( { getByTestId, disabled: true } );
+ } );
+ } );
+
+ describe( 'when is selected', () => {
+ it( 'should render the disabled style', () => {
+ const { getByTestId } = renderRadioButton( { disabled: true, selected: true } );
+ const icon = getByTestId( `${TEST_ID}-icon` );
+ expect( hasChildWithProp( icon, 'stroke', '#B0B4CD' ) ).toBe( true );
+ accessibilityTest( {
+ getByTestId, disabled: true, checked: true
+ } );
+ } );
+ } );
+ } );
+} );
diff --git a/web_example/src/App.tsx b/web_example/src/App.tsx
index cd35d2e..69f7ccb 100644
--- a/web_example/src/App.tsx
+++ b/web_example/src/App.tsx
@@ -13,6 +13,7 @@ import ButtonExamples from './components/ButtonExamples';
import IconExamples from './components/IconExamples';
import IconButtonExamples from './components/IconButtonExamples';
import CheckboxExamples from './components/CheckboxExamples';
+import RadioButtonExamples from './components/RadioButtonExamples';
const customTheme = extendThemeConfig( {
palette: {
@@ -96,6 +97,7 @@ export default function App() {
+
diff --git a/web_example/src/components/RadioButtonExamples.tsx b/web_example/src/components/RadioButtonExamples.tsx
new file mode 100644
index 0000000..79a0345
--- /dev/null
+++ b/web_example/src/components/RadioButtonExamples.tsx
@@ -0,0 +1,64 @@
+/* eslint-disable no-alert */
+import * as React from 'react';
+
+import { StyleSheet, View } from 'react-native';
+import {
+ RadioButton, VStack, Text, HStack
+} from '@amalgama/react-native-ui-kit';
+
+const styles = StyleSheet.create( {
+ container: {
+ width: '100%',
+ marginBottom: 20
+ },
+ separator: {
+ height: 1,
+ minWidth: '100%',
+ marginTop: 2,
+ marginBottom: 6,
+ backgroundColor: 'black'
+ },
+ vspace: {
+ height: 10,
+ minWidth: '100%'
+ }
+} );
+
+const RadioButtonExamples = () => {
+ const [ selected, setSelected ] = React.useState( false );
+ return (
+
+ RadioButton Component
+
+ Enabled
+
+
+
+
+
+
+
+
+ Disabled
+
+
+
+
+
+
+
+
+ On press
+
+
+
+ setSelected( prev => !prev )} />
+
+
+
+
+
+ );
+};
+
+export default RadioButtonExamples;