Skip to content
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

Merged
merged 6 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/Icon/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,4 @@ class Icon extends PureComponent<IconProps> {
}

export default withTheme(withThemeStyles(withStyleUtils(Icon)));
export type {SrcProps};
50 changes: 0 additions & 50 deletions src/components/TabSelector/TabIcon.js

This file was deleted.

43 changes: 43 additions & 0 deletions src/components/TabSelector/TabIcon.tsx
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 */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/** Animated opacity value while the label is inactive state */
/** Animated opacity value while the icon is in inactive state */

inactiveOpacity?: number | Animated.AnimatedInterpolation<number>;

/** Animated opacity value while the label is in active state */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/** Animated opacity value while the label is in active state */
/** Animated opacity value while the icon is in active state */

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/** Animated opacity value while the label is inactive state */
/** Animated opacity value while the label is in inactive state */

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>
Expand All @@ -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,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<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: () => {},
},
};

const getIconAndTitle = (route, translate) => {
function getIconAndTitle(route: string, translate: LocaleContextProps['translate']): {icon: FunctionComponent<SvgProps>; title: string} {
switch (route) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's create a separate type for return type {icon: FunctionComponent<SvgProps>; title: string}

case CONST.TAB_REQUEST.MANUAL:
return {icon: Expensicons.Pencil, title: translate('tabSelector.manual')};
Expand All @@ -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<number>, routesLength: number, tabIndex: number, active: boolean, affectedTabs: number[]) {
const activeValue = active ? 1 : 0;
const inactiveValue = active ? 0 : 1;

Expand All @@ -62,35 +53,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);
Expand All @@ -99,7 +90,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);
Expand All @@ -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});
}
Comment on lines -124 to +120
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you clarify why this was changed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
navigation.navigate({name: route.name, merge: true, params: {}); because params is not optional in navigate function arguments. Thats why I switched to navigation.navigate({key: route.key, merge: true}); usage, because it does the same thing, but has optional params argument.


onTabPress(route.name);
Expand All @@ -144,8 +135,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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/** Animated opacity value while the label is inactive state */
/** Animated opacity value while the tab is in inactive state */

inactiveOpacity: PropTypes.any,
inactiveOpacity?: number | Animated.AnimatedInterpolation<number>;

/** Animated opacity value while the label is in active state */
// eslint-disable-next-line
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/** Animated opacity value while the label is in active state */
/** Animated opacity value while the tab is in active state */

activeOpacity: PropTypes.any,
activeOpacity?: number | Animated.AnimatedInterpolation<number>;

/** Whether this tab is active */
isFocused: PropTypes.bool,
isFocused?: boolean;
};
Copy link
Contributor

Choose a reason for hiding this comment

The 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
isFocused?: boolean;
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, isFocused = false}: TabSelectorItemProps) {
const styles = useThemeStyles();
return (
<PressableWithFeedback
Expand All @@ -69,8 +56,6 @@ function TabSelectorItem({icon, title, onPress, backgroundColor, activeOpacity,
);
}

TabSelectorItem.propTypes = propTypes;
TabSelectorItem.defaultProps = defaultProps;
TabSelectorItem.displayName = 'TabSelectorItem';

export default TabSelectorItem;
9 changes: 7 additions & 2 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3626,11 +3626,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<string>) => ({
backgroundColor: hovered && !isFocused ? theme.highlightBG : background,
}),

tabOpacity: (hovered: boolean, isFocused: boolean, activeOpacityValue: number, inactiveOpacityValue: number) => ({
tabOpacity: (
hovered: boolean,
isFocused: boolean,
activeOpacityValue: number | Animated.AnimatedInterpolation<number>,
inactiveOpacityValue: number | Animated.AnimatedInterpolation<number>,
) => ({
opacity: hovered && !isFocused ? inactiveOpacityValue : activeOpacityValue,
}),

Expand Down
Loading