diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index ee54135042..6ec539ecd4 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -155,9 +155,7 @@ const config = { ProgressBar: 'ProgressBar', RadioButton: { RadioButton: 'RadioButton/RadioButton', - RadioButtonAndroid: 'RadioButton/RadioButtonAndroid', RadioButtonGroup: 'RadioButton/RadioButtonGroup', - RadioButtonIOS: 'RadioButton/RadioButtonIOS', RadioButtonItem: 'RadioButton/RadioButtonItem', }, Searchbar: 'Searchbar', diff --git a/docs/versioned_docs/version-6.x/components/RadioButton/RadioButtonAndroid.mdx b/docs/versioned_docs/version-6.x/components/RadioButton/RadioButtonAndroid.mdx deleted file mode 100644 index 2faf286f16..0000000000 --- a/docs/versioned_docs/version-6.x/components/RadioButton/RadioButtonAndroid.mdx +++ /dev/null @@ -1,99 +0,0 @@ ---- -title: RadioButton.Android ---- - -import PropTable from '@site/src/components/PropTable.tsx'; -import ExtendsLink from '@site/src/components/ExtendsLink.tsx'; -import ThemeColorsTable from '@site/src/components/ThemeColorsTable.tsx'; -import ScreenshotTabs from '@site/src/components/ScreenshotTabs.tsx'; -import ExtendedExample from '@site/src/components/ExtendedExample.tsx'; - - - - - - - -Radio buttons allow the selection a single option from a set. -This component follows platform guidelines for Android, but can be used -on any platform. - - - - - ## Props - ### TouchableRipple props - - - -
- -### value (required) - -
- - - -
- -### status - -
- - - -
- -### disabled - -
- - - -
- -### onPress - -
- - - -
- -### uncheckedColor - -
- - - -
- -### color - -
- - - -
- -### theme - -
- - - -
- -### testID - -
- - - - - - - - - - diff --git a/docs/versioned_docs/version-6.x/components/RadioButton/RadioButtonIOS.mdx b/docs/versioned_docs/version-6.x/components/RadioButton/RadioButtonIOS.mdx deleted file mode 100644 index 83db4152cc..0000000000 --- a/docs/versioned_docs/version-6.x/components/RadioButton/RadioButtonIOS.mdx +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: RadioButton.IOS ---- - -import PropTable from '@site/src/components/PropTable.tsx'; -import ExtendsLink from '@site/src/components/ExtendsLink.tsx'; -import ThemeColorsTable from '@site/src/components/ThemeColorsTable.tsx'; -import ScreenshotTabs from '@site/src/components/ScreenshotTabs.tsx'; -import ExtendedExample from '@site/src/components/ExtendedExample.tsx'; - - - - - - - -Radio buttons allow the selection a single option from a set. -This component follows platform guidelines for iOS, but can be used -on any platform. - - - - - ## Props - ### TouchableRipple props - - - -
- -### value (required) - -
- - - -
- -### status - -
- - - -
- -### disabled - -
- - - -
- -### onPress - -
- - - -
- -### color - -
- - - -
- -### theme - -
- - - -
- -### testID - -
- - - - - - - - - - diff --git a/docs/versioned_docs/version-6.x/components/RadioButton/RadioButtonItem.mdx b/docs/versioned_docs/version-6.x/components/RadioButton/RadioButtonItem.mdx index c5f863d44b..34aaf28c73 100644 --- a/docs/versioned_docs/version-6.x/components/RadioButton/RadioButtonItem.mdx +++ b/docs/versioned_docs/version-6.x/components/RadioButton/RadioButtonItem.mdx @@ -171,14 +171,6 @@ export default MyComponent;
-### mode - -
- - - -
- ### position
diff --git a/example/src/Examples/RadioButtonExample.tsx b/example/src/Examples/RadioButtonExample.tsx index 6ab1869dd9..c30e49e00f 100644 --- a/example/src/Examples/RadioButtonExample.tsx +++ b/example/src/Examples/RadioButtonExample.tsx @@ -10,7 +10,7 @@ import { import ScreenWrapper from '../ScreenWrapper'; -type State = 'normal' | 'normal-ios' | 'normal-item' | 'custom'; +type State = 'normal' | 'normal-item' | 'custom'; const RadioButtonExample = () => { const [checked, setChecked] = React.useState('normal'); @@ -19,26 +19,15 @@ const RadioButtonExample = () => { setChecked('normal')}> - Normal - Material Design + Normal - - setChecked('normal-ios')}> - - Normal 2 - IOS - - - - - setChecked('custom')}> Custom @@ -58,11 +47,31 @@ const RadioButtonExample = () => { onPress={() => setChecked('normal-item')} /> - Checked (Disabled) + Error (Checked) + + + + Error (Unchecked) + + + + + + Checked (Disabled) - Unchecked (Disabled) + Unchecked (Disabled) { Second - + Third - + diff --git a/example/src/Examples/RadioButtonItemExample.tsx b/example/src/Examples/RadioButtonItemExample.tsx index e640a1dd0a..efd4eee32a 100644 --- a/example/src/Examples/RadioButtonItemExample.tsx +++ b/example/src/Examples/RadioButtonItemExample.tsx @@ -7,55 +7,47 @@ import ScreenWrapper from '../ScreenWrapper'; const RadioButtonItemExample = () => { const [checkedDefault, setCheckedDefault] = React.useState(true); - const [checkedAndroid, setCheckedAndroid] = React.useState(true); - const [checkedIOS, setCheckedIOS] = React.useState(true); const [checkedLeadingControl, setCheckedLeadingControl] = React.useState(true); const [checkedDisabled, setCheckedDisabled] = React.useState(true); + const [checkedError, setCheckedError] = React.useState(true); const [checkedLabelVariant, setCheckedLabelVariant] = React.useState(true); return ( setCheckedDefault(!checkedDefault)} value="default" /> setCheckedAndroid(!checkedAndroid)} - value="android" - /> - setCheckedIOS(!checkedIOS)} - value="iOS" - /> - setCheckedLeadingControl(!checkedLeadingControl)} - value="iOS" + value="leading" position="leading" /> setCheckedError(!checkedError)} + value="error" + error + /> + setCheckedDisabled(!checkedDisabled)} - value="iOS" + value="disabled" disabled /> setCheckedLabelVariant(!checkedLabelVariant)} - value="default" + value="variant" /> ); diff --git a/src/components/RadioButton/RadioButton.tsx b/src/components/RadioButton/RadioButton.tsx index cb8a621e2d..384f734d3d 100644 --- a/src/components/RadioButton/RadioButton.tsx +++ b/src/components/RadioButton/RadioButton.tsx @@ -1,12 +1,23 @@ -import { Platform } from 'react-native'; -import type { GestureResponderEvent } from 'react-native'; +import * as React from 'react'; +import { StyleSheet, View } from 'react-native'; -import RadioButtonAndroid from './RadioButtonAndroid'; -import RadioButtonIOS from './RadioButtonIOS'; +import Animated, { + Easing, + ReduceMotion, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated'; + +import { RadioButtonContext } from './RadioButtonGroup'; +import { RadioButtonTokens } from './tokens'; +import { getSelectionControlColor, handlePress, isChecked } from './utils'; import { useInternalTheme } from '../../core/theming'; -import type { ThemeProp } from '../../types'; +import { useReduceMotion } from '../../theme/accessibility/ReduceMotionContext'; +import type { $RemoveChildren, ThemeProp } from '../../types'; +import TouchableRipple from '../TouchableRipple/TouchableRipple'; -export type Props = { +export type Props = $RemoveChildren & { /** * Value of the radio button */ @@ -22,7 +33,7 @@ export type Props = { /** * Function to execute on press. */ - onPress?: (e: GestureResponderEvent) => void; + onPress?: (param?: any) => void; /** * Custom color for unchecked radio. */ @@ -31,6 +42,12 @@ export type Props = { * Custom color for radio. */ color?: string; + /** + * Whether the radio button is in an error state. When true, the ring + * (unchecked) and dot (selected) use `theme.colors.error`. `disabled` + * and explicit `color`/`uncheckedColor` overrides take precedence. + */ + error?: boolean; /** * @optional */ @@ -41,6 +58,8 @@ export type Props = { testID?: string; }; +const { ringSize, dotSize, outlineWidth: OUTLINE_WIDTH } = RadioButtonTokens; + /** * Radio buttons allow the selection a single option from a set. * @@ -71,16 +90,159 @@ export type Props = { * * export default MyComponent; * ``` + * + * @extends TouchableRipple props https://callstack.github.io/react-native-paper/docs/components/TouchableRipple */ -const RadioButton = ({ theme: themeOverrides, ...props }: Props) => { +const RadioButton = ({ + disabled, + onPress, + theme: themeOverrides, + value, + status, + testID, + error, + ...rest +}: Props) => { const theme = useInternalTheme(themeOverrides); + const context = React.useContext(RadioButtonContext); + + const checked = + isChecked({ + contextValue: context?.value, + status, + value, + }) === 'checked'; + + const reduceMotion = useReduceMotion(); + const reanimatedReduceMotion = reduceMotion + ? ReduceMotion.Always + : ReduceMotion.Never; + + // Single selection animation path: the dot scales in (with a slight + // overshoot) when the radio becomes checked. The ring outline stays a + // constant width. Keyed on `checked` (not `status`) so radios driven by a + // `RadioButton.Group` animate too. + const dotScale = useSharedValue(1); + const isFirstRendering = React.useRef(true); - const Button = Platform.select({ - default: RadioButtonAndroid, - ios: RadioButtonIOS, - }); + const dotTimingConfig = React.useMemo( + () => ({ + duration: theme.motion.duration.short3, + easing: Easing.bezier(...theme.motion.easing.standard), + reduceMotion: reanimatedReduceMotion, + }), + [ + theme.motion.duration.short3, + theme.motion.easing.standard, + reanimatedReduceMotion, + ] + ); - return