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..f61001145029 --- /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 icon is in inactive state */ + inactiveOpacity?: number | Animated.AnimatedInterpolation; + + /** Animated opacity value while the icon 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 59% rename from src/components/TabSelector/TabLabel.js rename to src/components/TabSelector/TabLabel.tsx index fdf204011152..40f4dc30bb97 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, + /** Animated opacity value while the label is in inactive state */ + 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 70% rename from src/components/TabSelector/TabSelector.js rename to src/components/TabSelector/TabSelector.tsx index 444bb62263d9..28ab15c04e9b 100644 --- a/src/components/TabSelector/TabSelector.js +++ b/src/components/TabSelector/TabSelector.tsx @@ -1,42 +1,38 @@ -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: () => {}, - }, +type IconAndTitle = { + icon: FunctionComponent; + title: string; }; -const getIconAndTitle = (route, translate) => { +function getIconAndTitle(route: string, translate: LocaleContextProps['translate']): IconAndTitle { switch (route) { case CONST.TAB_REQUEST.MANUAL: return {icon: Expensicons.Pencil, title: translate('tabSelector.manual')}; @@ -51,9 +47,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 +58,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 +72,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 +86,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,15 +95,15 @@ 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); - const isFocused = index === state.index; + const isActive = index === state.index; const {icon, title} = getIconAndTitle(route.name, translate); const onPress = () => { - if (isFocused) { + if (isActive) { return; } @@ -121,7 +117,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); @@ -136,7 +132,7 @@ function TabSelector({state, navigation, onTabPress, position}) { activeOpacity={activeOpacity} inactiveOpacity={inactiveOpacity} backgroundColor={backgroundColor} - isFocused={isFocused} + isActive={isActive} /> ); })} @@ -144,8 +140,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 55% rename from src/components/TabSelector/TabSelectorItem.js rename to src/components/TabSelector/TabSelectorItem.tsx index 88aa98766fae..c10a7475504f 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, + /** Animated opacity value while the tab is in inactive state */ + inactiveOpacity?: number | Animated.AnimatedInterpolation; - /** Animated opacity value while the label is in active state */ - // eslint-disable-next-line - activeOpacity: PropTypes.any, + /** Animated opacity value while the tab is in active state */ + activeOpacity?: number | Animated.AnimatedInterpolation; /** Whether this tab is active */ - isFocused: PropTypes.bool, + isActive?: 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, isActive = false}: TabSelectorItemProps) { const styles = useThemeStyles(); return ( {({hovered}) => ( - + )} @@ -69,8 +56,6 @@ function TabSelectorItem({icon, title, onPress, backgroundColor, activeOpacity, ); } -TabSelectorItem.propTypes = propTypes; -TabSelectorItem.defaultProps = defaultProps; TabSelectorItem.displayName = 'TabSelectorItem'; export default TabSelectorItem; diff --git a/src/styles/index.ts b/src/styles/index.ts index 78d724fc4bc5..71f77689042d 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3686,11 +3686,16 @@ const styles = (theme: ThemeColors) => 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, }),