Skip to content

Commit

Permalink
Merge pull request #33084 from software-mansion-labs/@szymczak/25089
Browse files Browse the repository at this point in the history
  • Loading branch information
cead22 authored Dec 22, 2023
2 parents d70c932 + dbfb60a commit 6a55d16
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 136 deletions.
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 icon is in inactive state */
inactiveOpacity?: number | Animated.AnimatedInterpolation<number>;

/** 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
inactiveOpacity: PropTypes.any,
/** Animated opacity value while the label is in inactive state */
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,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')};
Expand All @@ -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;

Expand All @@ -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);
Expand All @@ -99,15 +95,15 @@ 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);
const isFocused = index === state.index;
const isActive = index === state.index;
const {icon, title} = getIconAndTitle(route.name, translate);

const onPress = () => {
if (isFocused) {
if (isActive) {
return;
}

Expand All @@ -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);
Expand All @@ -136,16 +132,14 @@ function TabSelector({state, navigation, onTabPress, position}) {
activeOpacity={activeOpacity}
inactiveOpacity={inactiveOpacity}
backgroundColor={backgroundColor}
isFocused={isFocused}
isActive={isActive}
/>
);
})}
</View>
);
}

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
inactiveOpacity: PropTypes.any,
/** Animated opacity value while the tab is in inactive state */
inactiveOpacity?: number | Animated.AnimatedInterpolation<number>;

/** 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<number>;

/** 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 (
<PressableWithFeedback
Expand All @@ -52,25 +39,23 @@ function TabSelectorItem({icon, title, onPress, backgroundColor, activeOpacity,
onPress={onPress}
>
{({hovered}) => (
<Animated.View style={[styles.tabSelectorButton, StyleSheet.absoluteFill, styles.tabBackground(hovered, isFocused, backgroundColor)]}>
<Animated.View style={[styles.tabSelectorButton, StyleSheet.absoluteFill, styles.tabBackground(hovered, isActive, backgroundColor)]}>
<TabIcon
icon={icon}
activeOpacity={styles.tabOpacity(hovered, isFocused, activeOpacity, inactiveOpacity).opacity}
inactiveOpacity={styles.tabOpacity(hovered, isFocused, inactiveOpacity, activeOpacity).opacity}
activeOpacity={styles.tabOpacity(hovered, isActive, activeOpacity, inactiveOpacity).opacity}
inactiveOpacity={styles.tabOpacity(hovered, isActive, inactiveOpacity, activeOpacity).opacity}
/>
<TabLabel
title={title}
activeOpacity={styles.tabOpacity(hovered, isFocused, activeOpacity, inactiveOpacity).opacity}
inactiveOpacity={styles.tabOpacity(hovered, isFocused, inactiveOpacity, activeOpacity).opacity}
activeOpacity={styles.tabOpacity(hovered, isActive, activeOpacity, inactiveOpacity).opacity}
inactiveOpacity={styles.tabOpacity(hovered, isActive, inactiveOpacity, activeOpacity).opacity}
/>
</Animated.View>
)}
</PressableWithFeedback>
);
}

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

export default TabSelectorItem;
Loading

0 comments on commit 6a55d16

Please sign in to comment.