From 2dca05b879948031f6b45c23c2f03497a7de1039 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Thu, 14 Dec 2023 17:37:41 +0100 Subject: [PATCH 1/3] migrate TabSelector components to typescript --- src/components/Icon/index.tsx | 1 + src/components/TabSelector/TabIcon.js | 50 ---------------- src/components/TabSelector/TabIcon.tsx | 43 ++++++++++++++ .../TabSelector/{TabLabel.js => TabLabel.tsx} | 21 ++----- .../{TabSelector.js => TabSelector.tsx} | 57 ++++++++----------- ...TabSelectorItem.js => TabSelectorItem.tsx} | 35 ++++-------- src/styles/index.ts | 9 ++- 7 files changed, 89 insertions(+), 127 deletions(-) delete mode 100644 src/components/TabSelector/TabIcon.js create mode 100644 src/components/TabSelector/TabIcon.tsx rename src/components/TabSelector/{TabLabel.js => TabLabel.tsx} (64%) rename src/components/TabSelector/{TabSelector.js => TabSelector.tsx} (73%) rename src/components/TabSelector/{TabSelectorItem.js => TabSelectorItem.tsx} (71%) diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index 80abe1872c12..9be549efac26 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -103,3 +103,4 @@ class Icon extends PureComponent { } export default withTheme(withThemeStyles(withStyleUtils(Icon))); +export type {SrcProps}; diff --git a/src/components/TabSelector/TabIcon.js b/src/components/TabSelector/TabIcon.js deleted file mode 100644 index d96ae19897f4..000000000000 --- a/src/components/TabSelector/TabIcon.js +++ /dev/null @@ -1,50 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {Animated, StyleSheet, View} from 'react-native'; -import Icon from '@components/Icon'; -import useTheme from '@hooks/useTheme'; - -const propTypes = { - /** Icon to display on tab */ - icon: PropTypes.func, - - /** Animated opacity value while the label is inactive state */ - // eslint-disable-next-line - inactiveOpacity: PropTypes.any, - - /** Animated opacity value while the label is in active state */ - // eslint-disable-next-line - activeOpacity: PropTypes.any, -}; - -const defaultProps = { - icon: '', - inactiveOpacity: 1, - activeOpacity: 0, -}; - -function TabIcon({icon, activeOpacity, inactiveOpacity}) { - const theme = useTheme(); - return ( - - - - - - - - - ); -} - -TabIcon.propTypes = propTypes; -TabIcon.defaultProps = defaultProps; -TabIcon.displayName = 'TabIcon'; - -export default TabIcon; diff --git a/src/components/TabSelector/TabIcon.tsx b/src/components/TabSelector/TabIcon.tsx new file mode 100644 index 000000000000..5759e0505224 --- /dev/null +++ b/src/components/TabSelector/TabIcon.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import {Animated, StyleSheet, View} from 'react-native'; +import Icon, {SrcProps} from '@components/Icon'; +import useTheme from '@hooks/useTheme'; + +type TabIconProps = { + /** Icon to display on tab */ + icon?: (props: SrcProps) => React.ReactNode; + + /** Animated opacity value while the label is inactive state */ + inactiveOpacity?: number | Animated.AnimatedInterpolation; + + /** Animated opacity value while the label is in active state */ + activeOpacity?: number | Animated.AnimatedInterpolation; +}; + +function TabIcon({icon, activeOpacity = 0, inactiveOpacity = 1}: TabIconProps) { + const theme = useTheme(); + return ( + + {icon && ( + <> + + + + + + + + )} + + ); +} + +TabIcon.displayName = 'TabIcon'; + +export default TabIcon; diff --git a/src/components/TabSelector/TabLabel.js b/src/components/TabSelector/TabLabel.tsx similarity index 64% rename from src/components/TabSelector/TabLabel.js rename to src/components/TabSelector/TabLabel.tsx index fdf204011152..871d57b1d048 100644 --- a/src/components/TabSelector/TabLabel.js +++ b/src/components/TabSelector/TabLabel.tsx @@ -1,28 +1,19 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {Animated, StyleSheet, Text, View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; -const propTypes = { +type TabLabelProps = { /** Title of the tab */ - title: PropTypes.string, + title?: string; /** Animated opacity value while the label is inactive state */ - // eslint-disable-next-line - inactiveOpacity: PropTypes.any, + inactiveOpacity?: number | Animated.AnimatedInterpolation; /** Animated opacity value while the label is in active state */ - // eslint-disable-next-line - activeOpacity: PropTypes.any, + activeOpacity?: number | Animated.AnimatedInterpolation; }; -const defaultProps = { - title: '', - inactiveOpacity: 1, - activeOpacity: 0, -}; - -function TabLabel({title, activeOpacity, inactiveOpacity}) { +function TabLabel({title = '', activeOpacity = 0, inactiveOpacity = 1}: TabLabelProps) { const styles = useThemeStyles(); return ( @@ -36,8 +27,6 @@ function TabLabel({title, activeOpacity, inactiveOpacity}) { ); } -TabLabel.propTypes = propTypes; -TabLabel.defaultProps = defaultProps; TabLabel.displayName = 'TabLabel'; export default TabLabel; diff --git a/src/components/TabSelector/TabSelector.js b/src/components/TabSelector/TabSelector.tsx similarity index 73% rename from src/components/TabSelector/TabSelector.js rename to src/components/TabSelector/TabSelector.tsx index 444bb62263d9..e3f4d33718a0 100644 --- a/src/components/TabSelector/TabSelector.js +++ b/src/components/TabSelector/TabSelector.tsx @@ -1,42 +1,33 @@ -import PropTypes from 'prop-types'; -import React, {useCallback, useMemo, useState} from 'react'; +import {MaterialTopTabNavigationHelpers} from '@react-navigation/material-top-tabs/lib/typescript/src/types'; +import {TabNavigationState} from '@react-navigation/native'; +import React, {FunctionComponent, useCallback, useEffect, useMemo, useState} from 'react'; +import type {Animated} from 'react-native'; import {View} from 'react-native'; -import _ from 'underscore'; +import {SvgProps} from 'react-native-svg'; import * as Expensicons from '@components/Icon/Expensicons'; +import {LocaleContextProps} from '@components/LocaleContextProvider'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import {RootStackParamList} from '@libs/Navigation/types'; import CONST from '@src/CONST'; import TabSelectorItem from './TabSelectorItem'; -const propTypes = { +type TabSelectorProps = { /* Navigation state provided by React Navigation */ - // eslint-disable-next-line react/forbid-prop-types - state: PropTypes.object.isRequired, + state: TabNavigationState; /* Navigation functions provided by React Navigation */ - navigation: PropTypes.shape({ - navigate: PropTypes.func.isRequired, - emit: PropTypes.func.isRequired, - }).isRequired, + navigation: MaterialTopTabNavigationHelpers; /* Callback fired when tab is pressed */ - onTabPress: PropTypes.func, + onTabPress?: (name: string) => void; /* AnimatedValue for the position of the screen while swiping */ - position: PropTypes.shape({ - interpolate: PropTypes.func.isRequired, - }), + position: Animated.AnimatedInterpolation; }; -const defaultProps = { - onTabPress: () => {}, - position: { - interpolate: () => {}, - }, -}; - -const getIconAndTitle = (route, translate) => { +function getIconAndTitle(route: string, translate: LocaleContextProps['translate']): {icon: FunctionComponent; title: string} { switch (route) { case CONST.TAB_REQUEST.MANUAL: return {icon: Expensicons.Pencil, title: translate('tabSelector.manual')}; @@ -51,9 +42,9 @@ const getIconAndTitle = (route, translate) => { default: throw new Error(`Route ${route} has no icon nor title set.`); } -}; +} -const getOpacity = (position, routesLength, tabIndex, active, affectedTabs) => { +function getOpacity(position: Animated.AnimatedInterpolation, routesLength: number, tabIndex: number, active: boolean, affectedTabs: number[]) { const activeValue = active ? 1 : 0; const inactiveValue = active ? 0 : 1; @@ -62,13 +53,13 @@ const getOpacity = (position, routesLength, tabIndex, active, affectedTabs) => { return position.interpolate({ inputRange, - outputRange: _.map(inputRange, (i) => (affectedTabs.includes(tabIndex) && i === tabIndex ? activeValue : inactiveValue)), + outputRange: inputRange.map((i) => (affectedTabs.includes(tabIndex) && i === tabIndex ? activeValue : inactiveValue)), }); } return activeValue; -}; +} -function TabSelector({state, navigation, onTabPress, position}) { +function TabSelector({state, navigation, onTabPress = () => {}, position}: TabSelectorProps) { const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); @@ -76,13 +67,13 @@ function TabSelector({state, navigation, onTabPress, position}) { const [affectedAnimatedTabs, setAffectedAnimatedTabs] = useState(defaultAffectedAnimatedTabs); const getBackgroundColor = useCallback( - (routesLength, tabIndex, affectedTabs) => { + (routesLength: number, tabIndex: number, affectedTabs: number[]) => { if (routesLength > 1) { const inputRange = Array.from({length: routesLength}, (v, i) => i); return position.interpolate({ inputRange, - outputRange: _.map(inputRange, (i) => (affectedTabs.includes(tabIndex) && i === tabIndex ? theme.border : theme.appBG)), + outputRange: inputRange.map((i) => (affectedTabs.includes(tabIndex) && i === tabIndex ? theme.border : theme.appBG)), }); } return theme.border; @@ -90,7 +81,7 @@ function TabSelector({state, navigation, onTabPress, position}) { [theme, position], ); - React.useEffect(() => { + useEffect(() => { // It is required to wait transition end to reset affectedAnimatedTabs because tabs style is still animating during transition. setTimeout(() => { setAffectedAnimatedTabs(defaultAffectedAnimatedTabs); @@ -99,7 +90,7 @@ function TabSelector({state, navigation, onTabPress, position}) { return ( - {_.map(state.routes, (route, index) => { + {state.routes.map((route, index) => { const activeOpacity = getOpacity(position, state.routes.length, index, true, affectedAnimatedTabs); const inactiveOpacity = getOpacity(position, state.routes.length, index, false, affectedAnimatedTabs); const backgroundColor = getBackgroundColor(state.routes.length, index, affectedAnimatedTabs); @@ -121,7 +112,7 @@ function TabSelector({state, navigation, onTabPress, position}) { if (!event.defaultPrevented) { // The `merge: true` option makes sure that the params inside the tab screen are preserved - navigation.navigate({name: route.name, merge: true}); + navigation.navigate({key: route.key, merge: true}); } onTabPress(route.name); @@ -144,8 +135,6 @@ function TabSelector({state, navigation, onTabPress, position}) { ); } -TabSelector.propTypes = propTypes; -TabSelector.defaultProps = defaultProps; TabSelector.displayName = 'TabSelector'; export default TabSelector; diff --git a/src/components/TabSelector/TabSelectorItem.js b/src/components/TabSelector/TabSelectorItem.tsx similarity index 71% rename from src/components/TabSelector/TabSelectorItem.js rename to src/components/TabSelector/TabSelectorItem.tsx index 88aa98766fae..9f679ab020ae 100644 --- a/src/components/TabSelector/TabSelectorItem.js +++ b/src/components/TabSelector/TabSelectorItem.tsx @@ -1,48 +1,35 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {Animated, StyleSheet} from 'react-native'; +import {SrcProps} from '@components/Icon'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import useThemeStyles from '@hooks/useThemeStyles'; import TabIcon from './TabIcon'; import TabLabel from './TabLabel'; -const propTypes = { +type TabSelectorItemProps = { /** Function to call when onPress */ - onPress: PropTypes.func, + onPress?: () => void; /** Icon to display on tab */ - icon: PropTypes.func, + icon?: (props: SrcProps) => React.ReactNode; /** Title of the tab */ - title: PropTypes.string, + title?: string; /** Animated background color value for the tab button */ - // eslint-disable-next-line - backgroundColor: PropTypes.any, + backgroundColor?: string | Animated.AnimatedInterpolation; /** Animated opacity value while the label is inactive state */ - // eslint-disable-next-line - inactiveOpacity: PropTypes.any, + inactiveOpacity?: number | Animated.AnimatedInterpolation; /** Animated opacity value while the label is in active state */ - // eslint-disable-next-line - activeOpacity: PropTypes.any, + activeOpacity?: number | Animated.AnimatedInterpolation; /** Whether this tab is active */ - isFocused: PropTypes.bool, + isFocused?: boolean; }; -const defaultProps = { - onPress: () => {}, - icon: () => {}, - title: '', - backgroundColor: '', - inactiveOpacity: 1, - activeOpacity: 0, - isFocused: false, -}; - -function TabSelectorItem({icon, title, onPress, backgroundColor, activeOpacity, inactiveOpacity, isFocused}) { +function TabSelectorItem({icon, title = '', onPress = () => {}, backgroundColor = '', activeOpacity = 0, inactiveOpacity = 1, isFocused = false}: TabSelectorItemProps) { const styles = useThemeStyles(); return ( color: isSelected ? theme.text : theme.textSupporting, } satisfies TextStyle), - tabBackground: (hovered: boolean, isFocused: boolean, background: string) => ({ + tabBackground: (hovered: boolean, isFocused: boolean, background: string | Animated.AnimatedInterpolation) => ({ backgroundColor: hovered && !isFocused ? theme.highlightBG : background, }), - tabOpacity: (hovered: boolean, isFocused: boolean, activeOpacityValue: number, inactiveOpacityValue: number) => ({ + tabOpacity: ( + hovered: boolean, + isFocused: boolean, + activeOpacityValue: number | Animated.AnimatedInterpolation, + inactiveOpacityValue: number | Animated.AnimatedInterpolation, + ) => ({ opacity: hovered && !isFocused ? inactiveOpacityValue : activeOpacityValue, }), From e3dfe186a9a13a1299086257d6abfd087b4cff25 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Mon, 18 Dec 2023 13:13:12 +0100 Subject: [PATCH 2/3] fix pr comments --- src/components/TabSelector/TabSelector.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/TabSelector/TabSelector.tsx b/src/components/TabSelector/TabSelector.tsx index e3f4d33718a0..77765fea8b4d 100644 --- a/src/components/TabSelector/TabSelector.tsx +++ b/src/components/TabSelector/TabSelector.tsx @@ -27,7 +27,12 @@ type TabSelectorProps = { position: Animated.AnimatedInterpolation; }; -function getIconAndTitle(route: string, translate: LocaleContextProps['translate']): {icon: FunctionComponent; title: string} { +type IconAndTitle = { + icon: FunctionComponent; + title: string; +}; + +function getIconAndTitle(route: string, translate: LocaleContextProps['translate']): IconAndTitle { switch (route) { case CONST.TAB_REQUEST.MANUAL: return {icon: Expensicons.Pencil, title: translate('tabSelector.manual')}; From dbfb60a6a1d58c819db990e256888fc896511df3 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Wed, 20 Dec 2023 18:10:13 +0100 Subject: [PATCH 3/3] fix pr comments --- src/components/TabSelector/TabIcon.tsx | 4 ++-- src/components/TabSelector/TabLabel.tsx | 2 +- src/components/TabSelector/TabSelector.tsx | 6 +++--- src/components/TabSelector/TabSelectorItem.tsx | 18 +++++++++--------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/components/TabSelector/TabIcon.tsx b/src/components/TabSelector/TabIcon.tsx index 5759e0505224..f61001145029 100644 --- a/src/components/TabSelector/TabIcon.tsx +++ b/src/components/TabSelector/TabIcon.tsx @@ -7,10 +7,10 @@ type TabIconProps = { /** Icon to display on tab */ icon?: (props: SrcProps) => React.ReactNode; - /** Animated opacity value while the label is inactive state */ + /** Animated opacity value while the icon is in inactive state */ inactiveOpacity?: number | Animated.AnimatedInterpolation; - /** Animated opacity value while the label is in active state */ + /** Animated opacity value while the icon is in active state */ activeOpacity?: number | Animated.AnimatedInterpolation; }; diff --git a/src/components/TabSelector/TabLabel.tsx b/src/components/TabSelector/TabLabel.tsx index 871d57b1d048..40f4dc30bb97 100644 --- a/src/components/TabSelector/TabLabel.tsx +++ b/src/components/TabSelector/TabLabel.tsx @@ -6,7 +6,7 @@ type TabLabelProps = { /** Title of the tab */ title?: string; - /** Animated opacity value while the label is inactive state */ + /** Animated opacity value while the label is in inactive state */ inactiveOpacity?: number | Animated.AnimatedInterpolation; /** Animated opacity value while the label is in active state */ diff --git a/src/components/TabSelector/TabSelector.tsx b/src/components/TabSelector/TabSelector.tsx index 77765fea8b4d..28ab15c04e9b 100644 --- a/src/components/TabSelector/TabSelector.tsx +++ b/src/components/TabSelector/TabSelector.tsx @@ -99,11 +99,11 @@ function TabSelector({state, navigation, onTabPress = () => {}, position}: TabSe const activeOpacity = getOpacity(position, state.routes.length, index, true, affectedAnimatedTabs); const inactiveOpacity = getOpacity(position, state.routes.length, index, false, affectedAnimatedTabs); const backgroundColor = getBackgroundColor(state.routes.length, index, affectedAnimatedTabs); - const isFocused = index === state.index; + const isActive = index === state.index; const {icon, title} = getIconAndTitle(route.name, translate); const onPress = () => { - if (isFocused) { + if (isActive) { return; } @@ -132,7 +132,7 @@ function TabSelector({state, navigation, onTabPress = () => {}, position}: TabSe activeOpacity={activeOpacity} inactiveOpacity={inactiveOpacity} backgroundColor={backgroundColor} - isFocused={isFocused} + isActive={isActive} /> ); })} diff --git a/src/components/TabSelector/TabSelectorItem.tsx b/src/components/TabSelector/TabSelectorItem.tsx index 9f679ab020ae..c10a7475504f 100644 --- a/src/components/TabSelector/TabSelectorItem.tsx +++ b/src/components/TabSelector/TabSelectorItem.tsx @@ -19,17 +19,17 @@ type TabSelectorItemProps = { /** Animated background color value for the tab button */ backgroundColor?: string | Animated.AnimatedInterpolation; - /** Animated opacity value while the label is inactive state */ + /** Animated opacity value while the tab is in inactive state */ inactiveOpacity?: number | Animated.AnimatedInterpolation; - /** Animated opacity value while the label is in active state */ + /** Animated opacity value while the tab is in active state */ activeOpacity?: number | Animated.AnimatedInterpolation; /** Whether this tab is active */ - isFocused?: boolean; + isActive?: boolean; }; -function TabSelectorItem({icon, title = '', onPress = () => {}, backgroundColor = '', activeOpacity = 0, inactiveOpacity = 1, isFocused = false}: TabSelectorItemProps) { +function TabSelectorItem({icon, title = '', onPress = () => {}, backgroundColor = '', activeOpacity = 0, inactiveOpacity = 1, isActive = false}: TabSelectorItemProps) { const styles = useThemeStyles(); return ( {}, backgroundColor onPress={onPress} > {({hovered}) => ( - + )}