-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[TS migration] Migrate TabSelector components to typescript #33084
Changes from 4 commits
2dca05b
2213e8b
db28c84
e3dfe18
920efa8
dbfb60a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -101,3 +101,4 @@ function Icon({ | |
Icon.displayName = 'Icon'; | ||
|
||
export default Icon; | ||
export type {SrcProps}; |
This file was deleted.
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -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<number>; | ||||||
|
||||||
/** Animated opacity value while the label is in active state */ | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
activeOpacity?: number | Animated.AnimatedInterpolation<number>; | ||||||
}; | ||||||
|
||||||
function TabIcon({icon, activeOpacity = 0, inactiveOpacity = 1}: TabIconProps) { | ||||||
const theme = useTheme(); | ||||||
return ( | ||||||
<View> | ||||||
{icon && ( | ||||||
<> | ||||||
<Animated.View style={{opacity: inactiveOpacity}}> | ||||||
<Icon | ||||||
src={icon} | ||||||
fill={theme.icon} | ||||||
/> | ||||||
</Animated.View> | ||||||
<Animated.View style={[StyleSheet.absoluteFill, {opacity: activeOpacity}]}> | ||||||
<Icon | ||||||
src={icon} | ||||||
fill={theme.iconMenu} | ||||||
/> | ||||||
</Animated.View> | ||||||
</> | ||||||
)} | ||||||
</View> | ||||||
); | ||||||
} | ||||||
|
||||||
TabIcon.displayName = 'TabIcon'; | ||||||
|
||||||
export default TabIcon; |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
inactiveOpacity: PropTypes.any, | ||||||
inactiveOpacity?: number | Animated.AnimatedInterpolation<number>; | ||||||
|
||||||
/** Animated opacity value while the label is in active state */ | ||||||
// eslint-disable-next-line | ||||||
activeOpacity: PropTypes.any, | ||||||
activeOpacity?: number | Animated.AnimatedInterpolation<number>; | ||||||
}; | ||||||
|
||||||
const defaultProps = { | ||||||
title: '', | ||||||
inactiveOpacity: 1, | ||||||
activeOpacity: 0, | ||||||
}; | ||||||
|
||||||
function TabLabel({title, activeOpacity, inactiveOpacity}) { | ||||||
function TabLabel({title = '', activeOpacity = 0, inactiveOpacity = 1}: TabLabelProps) { | ||||||
const styles = useThemeStyles(); | ||||||
return ( | ||||||
<View> | ||||||
|
@@ -36,8 +27,6 @@ function TabLabel({title, activeOpacity, inactiveOpacity}) { | |||||
); | ||||||
} | ||||||
|
||||||
TabLabel.propTypes = propTypes; | ||||||
TabLabel.defaultProps = defaultProps; | ||||||
TabLabel.displayName = 'TabLabel'; | ||||||
|
||||||
export default TabLabel; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<RootStackParamList>; | ||
|
||
/* 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<number | string>; | ||
}; | ||
|
||
const defaultProps = { | ||
onTabPress: () => {}, | ||
position: { | ||
interpolate: () => {}, | ||
}, | ||
type IconAndTitle = { | ||
icon: FunctionComponent<SvgProps>; | ||
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<number>, routesLength: number, tabIndex: number, active: boolean, affectedTabs: number[]) { | ||
const activeValue = active ? 1 : 0; | ||
const inactiveValue = active ? 0 : 1; | ||
|
||
|
@@ -62,35 +58,35 @@ 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(); | ||
const defaultAffectedAnimatedTabs = useMemo(() => Array.from({length: state.routes.length}, (v, i) => i), [state.routes.length]); | ||
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; | ||
}, | ||
[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 +95,7 @@ function TabSelector({state, navigation, onTabPress, position}) { | |
|
||
return ( | ||
<View style={styles.tabSelector}> | ||
{_.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 +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}); | ||
} | ||
Comment on lines
-124
to
+120
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you clarify why this was changed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The previous usage wasn't interpreted by typescript properly, I would have to use |
||
|
||
onTabPress(route.name); | ||
|
@@ -144,8 +140,6 @@ function TabSelector({state, navigation, onTabPress, position}) { | |
); | ||
} | ||
|
||
TabSelector.propTypes = propTypes; | ||
TabSelector.defaultProps = defaultProps; | ||
TabSelector.displayName = 'TabSelector'; | ||
|
||
export default TabSelector; |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -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<string>; | ||||||
|
||||||
/** Animated opacity value while the label is inactive state */ | ||||||
// eslint-disable-next-line | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
inactiveOpacity: PropTypes.any, | ||||||
inactiveOpacity?: number | Animated.AnimatedInterpolation<number>; | ||||||
|
||||||
/** Animated opacity value while the label is in active state */ | ||||||
// eslint-disable-next-line | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
activeOpacity: PropTypes.any, | ||||||
activeOpacity?: number | Animated.AnimatedInterpolation<number>; | ||||||
|
||||||
/** Whether this tab is active */ | ||||||
isFocused: PropTypes.bool, | ||||||
isFocused?: boolean; | ||||||
}; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we call this isActive instead, since the comment says "Whether this tab is active", and the comments I'm suggesting changes on also use "active" and not "focused"?
Suggested change
|
||||||
|
||||||
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 ( | ||||||
<PressableWithFeedback | ||||||
|
@@ -69,8 +56,6 @@ function TabSelectorItem({icon, title, onPress, backgroundColor, activeOpacity, | |||||
); | ||||||
} | ||||||
|
||||||
TabSelectorItem.propTypes = propTypes; | ||||||
TabSelectorItem.defaultProps = defaultProps; | ||||||
TabSelectorItem.displayName = 'TabSelectorItem'; | ||||||
|
||||||
export default TabSelectorItem; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.