From 5e68e060c73ea8b7f181b422c52864cf5b18a839 Mon Sep 17 00:00:00 2001 From: Johnatan Botero Date: Wed, 23 Oct 2024 09:26:55 -0500 Subject: [PATCH 1/2] Stricter typing for SegmentedButtons component Related to #4528 Add stricter typing for `onValueChange` in `SegmentedButtons` component to support union types in TypeScript projects. * Modify `src/components/SegmentedButtons/SegmentedButtons.tsx` to accept a generic type parameter for the `value` and `onValueChange` props. * Update `ConditionalValue` type to use the generic type parameter. * Update `Props` type to use the generic type parameter. * Update example files to demonstrate the usage of the stricter typing: * `example/src/Examples/SegmentedButtons/SegmentedButtonDefault.tsx` * `example/src/Examples/SegmentedButtons/SegmentedButtonWithSelectedCheck.tsx` * `example/src/Examples/SegmentedButtons/SegmentedButtonWithDensity.tsx` * `example/src/Examples/SegmentedButtons/SegmentedButtonMultiselect.tsx` * `example/src/Examples/SegmentedButtons/SegmentedButtonMultiselectIcons.tsx` * `example/src/Examples/SegmentedButtons/SegmentedButtonCustomColorCheck.tsx` * `example/src/Examples/SegmentedButtons/SegmentedButtonDisabled.tsx` * `example/src/Examples/SegmentedButtons/SegmentedButtonMultiselectRealCase.tsx` * `example/src/Examples/SegmentedButtons/SegmentedButtonOnlyIcons.tsx` * `example/src/Examples/SegmentedButtons/SegmentedButtonOnlyIconsWithCheck.tsx` * `example/src/Examples/SegmentedButtons/SegmentedButtonRealCase.tsx` * `example/src/Examples/SegmentedButtonsExample.tsx` * `example/src/Examples/ToggleButtonExample.tsx` --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/callstack/react-native-paper/issues/4528?shareId=XXXX-XXXX-XXXX-XXXX). --- .../SegmentedButtonCustomColorCheck.tsx | 10 ++++++---- .../SegmentedButtonDefault.tsx | 6 ++++-- .../SegmentedButtonDisabled.tsx | 6 ++++-- .../SegmentedButtonMultiselect.tsx | 6 ++++-- .../SegmentedButtonMultiselectIcons.tsx | 6 ++++-- .../SegmentedButtonMultiselectRealCase.tsx | 8 +++++--- .../SegmentedButtonOnlyIcons.tsx | 8 +++++--- .../SegmentedButtonOnlyIconsWithCheck.tsx | 6 ++++-- .../SegmentedButtonRealCase.tsx | 6 ++++-- .../SegmentedButtonWithDensity.tsx | 6 ++++-- .../SegmentedButtonWithSelectedCheck.tsx | 6 ++++-- .../SegmentedButtons/SegmentedButtons.tsx | 20 +++++++++---------- 12 files changed, 58 insertions(+), 36 deletions(-) diff --git a/example/src/Examples/SegmentedButtons/SegmentedButtonCustomColorCheck.tsx b/example/src/Examples/SegmentedButtons/SegmentedButtonCustomColorCheck.tsx index 919865d28b..05c4cb607a 100644 --- a/example/src/Examples/SegmentedButtons/SegmentedButtonCustomColorCheck.tsx +++ b/example/src/Examples/SegmentedButtons/SegmentedButtonCustomColorCheck.tsx @@ -3,6 +3,8 @@ import { StyleSheet } from 'react-native'; import { List, SegmentedButtons } from 'react-native-paper'; +type TransportMode = 'walk' | 'train' | 'drive'; + const themeMock = { colors: { onSurface: '#3700B3', @@ -12,13 +14,13 @@ const themeMock = { }; const SegmentButtonCustomColorCheck = () => { - const [themeValue, setThemeValue] = React.useState(''); - const [colorValue, setColorValue] = React.useState(''); + const [themeValue, setThemeValue] = React.useState('walk'); + const [colorValue, setColorValue] = React.useState('walk'); return ( Via Theme - value={themeValue} onValueChange={setThemeValue} theme={themeMock} @@ -46,7 +48,7 @@ const SegmentButtonCustomColorCheck = () => { style={styles.group} /> Via Props - value={colorValue} onValueChange={setColorValue} theme={themeMock} diff --git a/example/src/Examples/SegmentedButtons/SegmentedButtonDefault.tsx b/example/src/Examples/SegmentedButtons/SegmentedButtonDefault.tsx index e05e56566c..79692014f7 100644 --- a/example/src/Examples/SegmentedButtons/SegmentedButtonDefault.tsx +++ b/example/src/Examples/SegmentedButtons/SegmentedButtonDefault.tsx @@ -3,12 +3,14 @@ import { StyleSheet } from 'react-native'; import { List, SegmentedButtons } from 'react-native-paper'; +type TransportMode = 'walk' | 'train' | 'drive'; + const SegmentedButtonDefault = () => { - const [value, setValue] = React.useState(''); + const [value, setValue] = React.useState('walk'); return ( - value={value} onValueChange={setValue} buttons={[ diff --git a/example/src/Examples/SegmentedButtons/SegmentedButtonDisabled.tsx b/example/src/Examples/SegmentedButtons/SegmentedButtonDisabled.tsx index 759ce1c925..aebc2fbf87 100644 --- a/example/src/Examples/SegmentedButtons/SegmentedButtonDisabled.tsx +++ b/example/src/Examples/SegmentedButtons/SegmentedButtonDisabled.tsx @@ -3,12 +3,14 @@ import { StyleSheet } from 'react-native'; import { List, SegmentedButtons } from 'react-native-paper'; +type TransportMode = 'walk' | 'disabled' | 'drive'; + const SegmentedButtonDisabled = () => { - const [value, setValue] = React.useState(''); + const [value, setValue] = React.useState('walk'); return ( - onValueChange={setValue} buttons={[ { diff --git a/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselect.tsx b/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselect.tsx index ee4929f6fa..d541d5cc76 100644 --- a/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselect.tsx +++ b/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselect.tsx @@ -3,12 +3,14 @@ import { StyleSheet } from 'react-native'; import { List, SegmentedButtons } from 'react-native-paper'; +type TransportMode = 'walk' | 'transit' | 'drive'; + const SegmentedButtonMultiselect = () => { - const [value, setValue] = React.useState([]); + const [value, setValue] = React.useState([]); return ( - multiSelect onValueChange={setValue} value={value} diff --git a/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselectIcons.tsx b/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselectIcons.tsx index 04f72d0c9e..e98e3bac1a 100644 --- a/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselectIcons.tsx +++ b/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselectIcons.tsx @@ -3,12 +3,14 @@ import { StyleSheet } from 'react-native'; import { List, SegmentedButtons } from 'react-native-paper'; +type Size = 'size-s' | 'size-m' | 'size-l' | 'size-xl' | 'size-xxl'; + const SegmentedButtonMultiselectIcons = () => { - const [value, setValue] = React.useState([]); + const [value, setValue] = React.useState([]); return ( - multiSelect onValueChange={setValue} value={value} diff --git a/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselectRealCase.tsx b/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselectRealCase.tsx index b2b0d7d999..7856c4d7d8 100644 --- a/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselectRealCase.tsx +++ b/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselectRealCase.tsx @@ -5,18 +5,20 @@ import { Card, IconButton, SegmentedButtons } from 'react-native-paper'; import { restaurantsData } from '../../../utils'; +type PriceRange = '1' | '2' | '3' | '4'; + const SegmentedButtonMultiselectRealCase = () => { - const [value, setValue] = React.useState([]); + const [value, setValue] = React.useState([]); const filteredData = React.useMemo( () => - restaurantsData.filter((item) => value.includes(item.price.toString())), + restaurantsData.filter((item) => value.includes(item.price.toString() as PriceRange)), [value] ); return ( - value={value} onValueChange={setValue} multiSelect diff --git a/example/src/Examples/SegmentedButtons/SegmentedButtonOnlyIcons.tsx b/example/src/Examples/SegmentedButtons/SegmentedButtonOnlyIcons.tsx index bf6cc9890f..2608762829 100644 --- a/example/src/Examples/SegmentedButtons/SegmentedButtonOnlyIcons.tsx +++ b/example/src/Examples/SegmentedButtons/SegmentedButtonOnlyIcons.tsx @@ -3,12 +3,14 @@ import { StyleSheet } from 'react-native'; import { List, SegmentedButtons } from 'react-native-paper'; +type TransportMode = 'walk' | 'train' | 'drive'; + const SegmentedButtonOnlyIcons = () => { - const [value, setValue] = React.useState(''); + const [value, setValue] = React.useState('walk'); return ( - onValueChange={setValue} style={styles.group} value={value} @@ -19,7 +21,7 @@ const SegmentedButtonOnlyIcons = () => { }, { icon: 'train', - value: 'trainsit', + value: 'train', }, { icon: 'car', diff --git a/example/src/Examples/SegmentedButtons/SegmentedButtonOnlyIconsWithCheck.tsx b/example/src/Examples/SegmentedButtons/SegmentedButtonOnlyIconsWithCheck.tsx index 640a20371b..41ad4c7bbc 100644 --- a/example/src/Examples/SegmentedButtons/SegmentedButtonOnlyIconsWithCheck.tsx +++ b/example/src/Examples/SegmentedButtons/SegmentedButtonOnlyIconsWithCheck.tsx @@ -3,12 +3,14 @@ import { StyleSheet } from 'react-native'; import { List, SegmentedButtons } from 'react-native-paper'; +type TransportMode = 'walk' | 'transit' | 'drive'; + const SegmentedButtonOnlyIconsWithCheck = () => { - const [value, setValue] = React.useState(''); + const [value, setValue] = React.useState('walk'); return ( - onValueChange={setValue} style={styles.group} value={value} diff --git a/example/src/Examples/SegmentedButtons/SegmentedButtonRealCase.tsx b/example/src/Examples/SegmentedButtons/SegmentedButtonRealCase.tsx index 6e24717635..b6b32c984e 100644 --- a/example/src/Examples/SegmentedButtons/SegmentedButtonRealCase.tsx +++ b/example/src/Examples/SegmentedButtons/SegmentedButtonRealCase.tsx @@ -5,12 +5,14 @@ import { Card, IconButton, SegmentedButtons } from 'react-native-paper'; import { songsData, albumsData } from '../../../utils'; +type MediaType = 'songs' | 'albums'; + const SegmentedButtonRealCase = () => { - const [value, setValue] = React.useState('songs'); + const [value, setValue] = React.useState('songs'); return ( - value={value} onValueChange={setValue} buttons={[ diff --git a/example/src/Examples/SegmentedButtons/SegmentedButtonWithDensity.tsx b/example/src/Examples/SegmentedButtons/SegmentedButtonWithDensity.tsx index b583553e0d..27e9ec01bd 100644 --- a/example/src/Examples/SegmentedButtons/SegmentedButtonWithDensity.tsx +++ b/example/src/Examples/SegmentedButtons/SegmentedButtonWithDensity.tsx @@ -3,12 +3,14 @@ import { StyleSheet } from 'react-native'; import { List, SegmentedButtons } from 'react-native-paper'; +type TransportMode = 'walk' | 'transit' | 'drive'; + const SegmentedButtonWithDensity = () => { - const [value, setValue] = React.useState(''); + const [value, setValue] = React.useState('walk'); return ( - onValueChange={setValue} value={value} density="medium" diff --git a/example/src/Examples/SegmentedButtons/SegmentedButtonWithSelectedCheck.tsx b/example/src/Examples/SegmentedButtons/SegmentedButtonWithSelectedCheck.tsx index 8257dc3f6a..57261a3aaa 100644 --- a/example/src/Examples/SegmentedButtons/SegmentedButtonWithSelectedCheck.tsx +++ b/example/src/Examples/SegmentedButtons/SegmentedButtonWithSelectedCheck.tsx @@ -3,12 +3,14 @@ import { StyleSheet } from 'react-native'; import { List, SegmentedButtons } from 'react-native-paper'; +type TransportMode = 'walk' | 'train' | 'drive'; + const SegmentedButtonWithSelectedCheck = () => { - const [value, setValue] = React.useState(''); + const [value, setValue] = React.useState('walk'); return ( - onValueChange={setValue} value={value} style={styles.group} diff --git a/src/components/SegmentedButtons/SegmentedButtons.tsx b/src/components/SegmentedButtons/SegmentedButtons.tsx index e5ab79629f..d3612c9f7b 100644 --- a/src/components/SegmentedButtons/SegmentedButtons.tsx +++ b/src/components/SegmentedButtons/SegmentedButtons.tsx @@ -15,12 +15,12 @@ import { getDisabledSegmentedButtonStyle } from './utils'; import { useInternalTheme } from '../../core/theming'; import type { IconSource } from '../Icon'; -type ConditionalValue = +type ConditionalValue = | { /** * Array of the currently selected segmented button values. */ - value: string[]; + value: T[]; /** * Support multiple selected options. */ @@ -28,13 +28,13 @@ type ConditionalValue = /** * Function to execute on selection change */ - onValueChange: (value: string[]) => void; + onValueChange: (value: T[]) => void; } | { /** * Value of the currently selected segmented button. */ - value: string; + value: T; /** * Support multiple selected options. */ @@ -42,10 +42,10 @@ type ConditionalValue = /** * Function to execute on selection change */ - onValueChange: (value: string) => void; + onValueChange: (value: T) => void; }; -export type Props = { +export type Props = { /** * Buttons to display as options in toggle button. * Button should contain the following properties: @@ -62,7 +62,7 @@ export type Props = { * - `testID`: testID to be used on tests */ buttons: { - value: string; + value: T; icon?: IconSource; disabled?: boolean; accessibilityLabel?: string; @@ -81,7 +81,7 @@ export type Props = { density?: 'regular' | 'small' | 'medium' | 'high'; style?: StyleProp; theme?: ThemeProp; -} & ConditionalValue; +} & ConditionalValue; /** * Segmented buttons can be used to select options, switch views or sort elements.
@@ -126,7 +126,7 @@ export type Props = { * export default MyComponent; *``` */ -const SegmentedButtons = ({ +const SegmentedButtons = ({ value, onValueChange, buttons, @@ -134,7 +134,7 @@ const SegmentedButtons = ({ density, style, theme: themeOverrides, -}: Props) => { +}: Props) => { const theme = useInternalTheme(themeOverrides); return ( From 22539f1ee1400e398a76fccc447f489683b99e56 Mon Sep 17 00:00:00 2001 From: Johnatan Botero Date: Wed, 23 Oct 2024 09:49:22 -0500 Subject: [PATCH 2/2] Update SegmentedButtonMultiselectRealCase example to demonstrate stricter typing * Remove unnecessary filter function in `filteredData` calculation * Fix ESLint error by updating code formatting --- .../SegmentedButtons/SegmentedButtonMultiselectRealCase.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselectRealCase.tsx b/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselectRealCase.tsx index 7856c4d7d8..43e2bba58e 100644 --- a/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselectRealCase.tsx +++ b/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselectRealCase.tsx @@ -12,7 +12,8 @@ const SegmentedButtonMultiselectRealCase = () => { const filteredData = React.useMemo( () => - restaurantsData.filter((item) => value.includes(item.price.toString() as PriceRange)), + value.includes(item.price.toString() as PriceRange) + ), [value] );