diff --git a/src/components/TabSelector/TabSelector.tsx b/src/components/TabSelector/TabSelector.tsx index fcbdbaddad5d..3809460436e5 100644 --- a/src/components/TabSelector/TabSelector.tsx +++ b/src/components/TabSelector/TabSelector.tsx @@ -1,12 +1,13 @@ import {MaterialTopTabNavigationHelpers} from '@react-navigation/material-top-tabs/lib/typescript/src/types'; import {TabNavigationState} from '@react-navigation/native'; -import React, {useMemo} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import type {Animated} from 'react-native'; import {View} from 'react-native'; 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 tabNavigatorAnimationEnabled from '@libs/Navigation/tabNavigatorAnimationEnabled'; import {RootStackParamList} from '@libs/Navigation/types'; import CONST from '@src/CONST'; import IconAsset from '@src/types/utils/IconAsset'; @@ -21,6 +22,9 @@ type TabSelectorProps = { /* Callback fired when tab is pressed */ onTabPress?: (name: string) => void; + + /* AnimatedValue for the position of the screen while swiping */ + position: Animated.AnimatedInterpolation; }; type IconAndTitle = { @@ -45,21 +49,66 @@ function getIconAndTitle(route: string, translate: LocaleContextProps['translate } } -function TabSelector({state, navigation, onTabPress}: TabSelectorProps) { +function getOpacity(position: Animated.AnimatedInterpolation, routesLength: number, tabIndex: number, active: boolean, affectedTabs: number[]) { + const activeValue = active ? 1 : 0; + const inactiveValue = active ? 0 : 1; + + if (routesLength > 1) { + const inputRange = Array.from({length: routesLength}, (v, i) => i); + + return position.interpolate({ + inputRange, + outputRange: inputRange.map((i) => (affectedTabs.includes(tabIndex) && i === tabIndex ? activeValue : inactiveValue)), + }); + } + return activeValue; +} + +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 tabs = useMemo( - () => - state.routes.map((route, index) => { - const isFocused = index === state.index; + const getBackgroundColor = useCallback( + (routesLength: number, tabIndex: number, affectedTabs: number[]) => { + if (routesLength > 1) { + const inputRange = Array.from({length: routesLength}, (v, i) => i); + + return position.interpolate({ + inputRange, + outputRange: inputRange.map((i) => (affectedTabs.includes(tabIndex) && i === tabIndex ? theme.border : theme.appBG)), + }); + } + return theme.border; + }, + [theme, position], + ); + + useEffect(() => { + // It is required to wait transition end to reset affectedAnimatedTabs because tabs style is still animating during transition. + setTimeout(() => { + setAffectedAnimatedTabs(defaultAffectedAnimatedTabs); + }, CONST.ANIMATED_TRANSITION); + }, [defaultAffectedAnimatedTabs, state.index]); + + return ( + + {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 isActive = index === state.index; const {icon, title} = getIconAndTitle(route.name, translate); const onPress = () => { - if (isFocused) { + if (isActive) { return; } + setAffectedAnimatedTabs([state.index, index]); + const event = navigation.emit({ type: 'tabPress', target: route.key, @@ -71,7 +120,7 @@ function TabSelector({state, navigation, onTabPress}: TabSelectorProps) { navigation.navigate({key: route.key, merge: true}); } - onTabPress?.(route.name); + onTabPress(route.name); }; return ( @@ -80,15 +129,15 @@ function TabSelector({state, navigation, onTabPress}: TabSelectorProps) { icon={icon} title={title} onPress={onPress} - isFocused={isFocused} - animationEnabled={tabNavigatorAnimationEnabled} + activeOpacity={activeOpacity} + inactiveOpacity={inactiveOpacity} + backgroundColor={backgroundColor} + isActive={isActive} /> ); - }), - [navigation, onTabPress, state.index, state.routes, translate], + })} + ); - - return {tabs}; } TabSelector.displayName = 'TabSelector'; diff --git a/src/components/TabSelector/TabSelectorItem.tsx b/src/components/TabSelector/TabSelectorItem.tsx index 5e68d9b7e09c..a246e1a14b54 100644 --- a/src/components/TabSelector/TabSelectorItem.tsx +++ b/src/components/TabSelector/TabSelectorItem.tsx @@ -1,9 +1,7 @@ -import React, {useCallback, useEffect, useRef} from 'react'; +import React from 'react'; import {Animated, StyleSheet} from 'react-native'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import CONST from '@src/CONST'; import IconAsset from '@src/types/utils/IconAsset'; import TabIcon from './TabIcon'; import TabLabel from './TabLabel'; @@ -18,45 +16,21 @@ type TabSelectorItemProps = { /** Title of the tab */ title?: string; - /** Whether this tab is active */ - isFocused?: boolean; - - /** Whether animations should be skipped */ - animationEnabled?: boolean; -}; - -function TabSelectorItem({icon, title = '', onPress = () => {}, isFocused = false, animationEnabled = true}: TabSelectorItemProps) { - const focusValueRef = useRef(new Animated.Value(isFocused ? 1 : 0)); - const styles = useThemeStyles(); - const theme = useTheme(); - - useEffect(() => { - const focusValue = isFocused ? 1 : 0; + /** Animated background color value for the tab button */ + backgroundColor?: string | Animated.AnimatedInterpolation; - if (animationEnabled) { - return Animated.timing(focusValueRef.current, { - toValue: focusValue, - duration: CONST.ANIMATED_TRANSITION, - useNativeDriver: true, - }).start(); - } + /** Animated opacity value while the tab is in inactive state */ + inactiveOpacity?: number | Animated.AnimatedInterpolation; - focusValueRef.current.setValue(focusValue); - }, [animationEnabled, isFocused]); + /** Animated opacity value while the tab is in active state */ + activeOpacity?: number | Animated.AnimatedInterpolation; - const getBackgroundColorStyle = useCallback( - (hovered: boolean) => { - if (hovered && !isFocused) { - return {backgroundColor: theme.highlightBG}; - } - return {backgroundColor: focusValueRef.current.interpolate({inputRange: [0, 1], outputRange: [theme.appBG, theme.border]})}; - }, - [theme, isFocused], - ); - - const activeOpacityValue = focusValueRef.current; - const inactiveOpacityValue = focusValueRef.current.interpolate({inputRange: [0, 1], outputRange: [1, 0]}); + /** Whether this tab is active */ + isActive?: boolean; +}; +function TabSelectorItem({icon, title = '', onPress = () => {}, backgroundColor = '', activeOpacity = 0, inactiveOpacity = 1, isActive = false}: TabSelectorItemProps) { + const styles = useThemeStyles(); return ( {}, isFocused = fals onPress={onPress} > {({hovered}) => ( - + )} diff --git a/src/libs/Navigation/OnyxTabNavigator.tsx b/src/libs/Navigation/OnyxTabNavigator.tsx index 506c60900d2f..ef41269045c6 100644 --- a/src/libs/Navigation/OnyxTabNavigator.tsx +++ b/src/libs/Navigation/OnyxTabNavigator.tsx @@ -6,9 +6,6 @@ import {OnyxEntry} from 'react-native-onyx/lib/types'; import Tab from '@userActions/Tab'; import ONYXKEYS from '@src/ONYXKEYS'; import ChildrenProps from '@src/types/utils/ChildrenProps'; -import tabNavigatorAnimationEnabled from './tabNavigatorAnimationEnabled'; - -const screenOptions = {animationEnabled: tabNavigatorAnimationEnabled}; type OnyxTabNavigatorOnyxProps = { selectedTab: OnyxEntry; @@ -40,7 +37,6 @@ function OnyxTabNavigator({id, selectedTab = '', children, onTabSelected = () => {...rest} id={id} initialRouteName={selectedTab} - screenOptions={screenOptions} backBehavior="initialRoute" keyboardDismissMode="none" screenListeners={{ diff --git a/src/libs/Navigation/tabNavigatorAnimationEnabled/index.native.ts b/src/libs/Navigation/tabNavigatorAnimationEnabled/index.native.ts deleted file mode 100644 index 531df2999215..000000000000 --- a/src/libs/Navigation/tabNavigatorAnimationEnabled/index.native.ts +++ /dev/null @@ -1,3 +0,0 @@ -const tabNavigatorAnimationEnabled = true; - -export default tabNavigatorAnimationEnabled; diff --git a/src/libs/Navigation/tabNavigatorAnimationEnabled/index.ts b/src/libs/Navigation/tabNavigatorAnimationEnabled/index.ts deleted file mode 100644 index 6282734e15c8..000000000000 --- a/src/libs/Navigation/tabNavigatorAnimationEnabled/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -const tabNavigatorAnimationEnabled = false; - -export default tabNavigatorAnimationEnabled; diff --git a/src/pages/iou/MoneyRequestSelectorPage.js b/src/pages/iou/MoneyRequestSelectorPage.js index f1cfea83ce5d..7b87b50bb7f3 100644 --- a/src/pages/iou/MoneyRequestSelectorPage.js +++ b/src/pages/iou/MoneyRequestSelectorPage.js @@ -119,7 +119,13 @@ function MoneyRequestSelectorPage(props) { ( + + )} > color: isSelected ? theme.text : theme.textSupporting, lineHeight: 14, } satisfies TextStyle), + + tabBackground: (hovered: boolean, isFocused: boolean, background: string | Animated.AnimatedInterpolation) => ({ + backgroundColor: hovered && !isFocused ? theme.highlightBG : background, + }), + + tabOpacity: ( + hovered: boolean, + isFocused: boolean, + activeOpacityValue: number | Animated.AnimatedInterpolation, + inactiveOpacityValue: number | Animated.AnimatedInterpolation, + ) => ({ + opacity: hovered && !isFocused ? inactiveOpacityValue : activeOpacityValue, + }), + overscrollSpacer: (backgroundColor: string, height: number) => ({ backgroundColor,