diff --git a/docs/6.x/docs/guides/migration.md b/docs/6.x/docs/guides/migration.md index 72aee6d00f..a9477eddee 100644 --- a/docs/6.x/docs/guides/migration.md +++ b/docs/6.x/docs/guides/migration.md @@ -285,6 +285,24 @@ The `overlayAccessibilityLabel` prop was renamed to `dismissAccessibilityLabel`: ``` +### RadioButton + +`RadioButton` renders one Material 3 circular radio control on every platform. The platform-specific components and their prop types are gone. On iOS checked controls no longer render a checkmark; they use the same ring-and-dot indicator as other platforms. `RadioButton` now also forwards `TouchableRipple` props (`accessible`, `onFocus`, `onBlur`, `borderless`, `rippleColor`, ...). + +The control now occupies a 48×48dp layout box instead of 36×36dp, so rows containing it may become 12dp taller. The 20dp radio indicator is unchanged. `RadioButton.Item` rows grow from 52dp to 64dp. + +| v5 | v6 | +| ------------------------------------------------ | ------------------------------------------------------------------------ | +| `RadioButton.Android`, `RadioButton.IOS` | `RadioButton` | +| `RadioButtonAndroidProps`, `RadioButtonIOSProps` | `RadioButtonProps` | +| `RadioButton.Item` `mode="android" \| "ios"` | `mode` removed; `RadioButton.Item` always renders `RadioButton` | +| n/a | new `error` (uses `theme.colors.error`) and `style` (touch-target style) | + +```diff +- ++ +``` + ### Searchbar The misspelled `traileringIcon` props have been renamed: diff --git a/docs/component-docs.config.ts b/docs/component-docs.config.ts index 2cf106849c..1fca2ae954 100644 --- a/docs/component-docs.config.ts +++ b/docs/component-docs.config.ts @@ -5,6 +5,14 @@ import { extendedExamples } from './src/data/extendedExamples.ts'; import { screenshots } from './src/data/screenshots.ts'; import { themeColors } from './src/data/themeColors.ts'; +// 6.x-only overrides: the v5 iOS radio assets show a control this release +// removes, but `generate-5x-docs.ts` still renders them from the shared map. +const screenshotsV6: Record = { + ...screenshots, + 'RadioButton.Group': 'screenshots/radio-button-group-android.gif', +}; +delete screenshotsV6['RadioButton.Item']; + const __dirname = path.dirname(fileURLToPath(import.meta.url)); export type Page = @@ -125,9 +133,7 @@ const pages = { ProgressBar: 'ProgressBar', RadioButton: { RadioButton: 'RadioButton/RadioButton', - RadioButtonAndroid: 'RadioButton/RadioButtonAndroid', RadioButtonGroup: 'RadioButton/RadioButtonGroup', - RadioButtonIOS: 'RadioButton/RadioButtonIOS', RadioButtonItem: 'RadioButton/RadioButtonItem', }, Searchbar: 'Searchbar', @@ -179,7 +185,7 @@ const componentDocsConfig: ComponentDocsConfig = { }, knownIssues: {}, themeColors, - screenshots, + screenshots: screenshotsV6, extendedExamples, }, }; diff --git a/docs/src/data/screenshots.ts b/docs/src/data/screenshots.ts index b7c59d424d..47cdf889c8 100644 --- a/docs/src/data/screenshots.ts +++ b/docs/src/data/screenshots.ts @@ -107,12 +107,6 @@ export const screenshots = { Modal: 'screenshots/modal.gif', ProgressBar: 'screenshots/progress-bar.png', RadioButton: { - 'Android (enabled)': 'screenshots/radio-enabled.android.png', - 'Android (disabled)': 'screenshots/radio-disabled.android.png', - 'iOS (enabled)': 'screenshots/radio-enabled.ios.png', - 'iOS (disabled)': 'screenshots/radio-disabled.ios.png', - }, - 'RadioButton.Android': { enabled: 'screenshots/radio-enabled.android.png', disabled: 'screenshots/radio-disabled.android.png', }, @@ -120,10 +114,6 @@ export const screenshots = { Android: 'screenshots/radio-button-group-android.gif', iOS: 'screenshots/radio-button-group-ios.gif', }, - 'RadioButton.IOS': { - enabled: 'screenshots/radio-enabled.ios.png', - disabled: 'screenshots/radio-disabled.ios.png', - }, 'RadioButton.Item': 'screenshots/radio-item.ios.png', Searchbar: { bar: 'screenshots/searchbar.png', 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 af39325c33..68f94ee492 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 type { GestureResponderEvent, ViewStyle } from 'react-native'; -import RadioButtonAndroid from './RadioButtonAndroid'; -import RadioButtonIOS from './RadioButtonIOS'; +import Animated, { cubicBezier, type CSSStyle } from 'react-native-reanimated'; + +import { RadioButtonContext } from './RadioButtonGroup'; +import { RadioButtonTokens } from './tokens'; +import { getSelectionControlColor, handlePress, isChecked } from './utils'; import { useInternalTheme } from '../../core/theming'; +import { useReduceMotion } from '../../theme/accessibility/ReduceMotionContext'; import type { ThemeProp } from '../../theme/types'; +import type { PressableStateCallbackType } from '../TouchableRipple/Pressable'; +import TouchableRipple from '../TouchableRipple/TouchableRipple'; +import type { Props as TouchableRippleProps } from '../TouchableRipple/TouchableRipple'; -export type Props = { +export type Props = Omit< + React.PropsWithoutRef, + 'children' +> & { /** * Value of the radio button */ @@ -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,14 @@ export type Props = { testID?: string; }; +const { + ringSize, + dotSize, + outlineWidth: OUTLINE_WIDTH, + stateLayerSize, + touchTargetSize, +} = RadioButtonTokens; + /** * Radio buttons allow the selection a single option from a set. * @@ -71,16 +96,136 @@ 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 Button = Platform.select({ - default: RadioButtonAndroid, - ios: RadioButtonIOS, - }); + const reduceMotion = useReduceMotion(); - return