diff --git a/src/components/TabSelector/TabSelector.tsx b/src/components/TabSelector/TabSelector.tsx index b05e633842b16..16c29c7f51c90 100644 --- a/src/components/TabSelector/TabSelector.tsx +++ b/src/components/TabSelector/TabSelector.tsx @@ -28,16 +28,6 @@ type IconAndTitle = { function getIconAndTitle(route: string, translate: LocaleContextProps['translate']): IconAndTitle { switch (route) { - case CONST.DEBUG.DETAILS: - return {icon: Expensicons.Info, title: translate('debug.details')}; - case CONST.DEBUG.JSON: - return {icon: Expensicons.Eye, title: translate('debug.JSON')}; - case CONST.DEBUG.REPORT_ACTIONS: - return {icon: Expensicons.Document, title: translate('debug.reportActions')}; - case CONST.DEBUG.REPORT_ACTION_PREVIEW: - return {icon: Expensicons.Document, title: translate('debug.reportActionPreview')}; - case CONST.DEBUG.TRANSACTION_VIOLATIONS: - return {icon: Expensicons.Exclamation, title: translate('debug.violations')}; case CONST.TAB_REQUEST.MANUAL: return {icon: Expensicons.Pencil, title: translate('tabSelector.manual')}; case CONST.TAB_REQUEST.SCAN: diff --git a/src/components/TabSelector/getBackground/index.native.ts b/src/components/TabSelector/getBackground/index.native.ts index 09a9b3f347e63..2fd2a2ef6dd3b 100644 --- a/src/components/TabSelector/getBackground/index.native.ts +++ b/src/components/TabSelector/getBackground/index.native.ts @@ -1,15 +1,20 @@ import type {Animated} from 'react-native'; import type GetBackgroudColor from './types'; -const getBackgroundColor: GetBackgroudColor = ({routesLength, tabIndex, affectedTabs, theme, position}) => { +const getBackgroundColor: GetBackgroudColor = ({routesLength, tabIndex, affectedTabs, theme, position, isActive}) => { if (routesLength > 1) { const inputRange = Array.from({length: routesLength}, (v, i) => i); - return position?.interpolate({ - inputRange, - outputRange: inputRange.map((i) => { - return affectedTabs.includes(tabIndex) && i === tabIndex ? theme.border : theme.appBG; - }), - }) as unknown as Animated.AnimatedInterpolation; + + if (position) { + return position.interpolate({ + inputRange, + outputRange: inputRange.map((i) => { + return affectedTabs.includes(tabIndex) && i === tabIndex ? theme.border : theme.appBG; + }), + }) as unknown as Animated.AnimatedInterpolation; + } + + return affectedTabs.includes(tabIndex) && isActive ? theme.border : theme.appBG; } return theme.border; }; diff --git a/src/components/TabSelector/getBackground/types.ts b/src/components/TabSelector/getBackground/types.ts index f66ee37e9b730..a207c3bab35eb 100644 --- a/src/components/TabSelector/getBackground/types.ts +++ b/src/components/TabSelector/getBackground/types.ts @@ -28,7 +28,7 @@ type GetBackgroudColorConfig = { /** * The animated position interpolation. */ - position: Animated.AnimatedInterpolation; + position: Animated.AnimatedInterpolation | undefined; /** * Whether the tab is active. diff --git a/src/components/TabSelector/getOpacity/index.native.ts b/src/components/TabSelector/getOpacity/index.native.ts index a59d32c2db6e5..fcdb1d0fc31ef 100644 --- a/src/components/TabSelector/getOpacity/index.native.ts +++ b/src/components/TabSelector/getOpacity/index.native.ts @@ -1,16 +1,20 @@ import type GetOpacity from './types'; -const getOpacity: GetOpacity = ({routesLength, tabIndex, active, affectedTabs, position}) => { +const getOpacity: GetOpacity = ({routesLength, tabIndex, active, affectedTabs, position, isActive}) => { 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)), - }); + if (position) { + return position.interpolate({ + inputRange, + outputRange: inputRange.map((i) => (affectedTabs.includes(tabIndex) && i === tabIndex ? activeValue : inactiveValue)), + }); + } + + return affectedTabs.includes(tabIndex) && isActive ? activeValue : inactiveValue; } return activeValue; }; diff --git a/src/components/TabSelector/getOpacity/types.ts b/src/components/TabSelector/getOpacity/types.ts index 46e4568b27838..a15eacf0d8ccb 100644 --- a/src/components/TabSelector/getOpacity/types.ts +++ b/src/components/TabSelector/getOpacity/types.ts @@ -27,7 +27,7 @@ type GetOpacityConfig = { /** * Scene's position, value which we would like to interpolate. */ - position: Animated.AnimatedInterpolation; + position: Animated.AnimatedInterpolation | undefined; /** * Whether the tab is active. diff --git a/src/libs/Navigation/DebugTabNavigator.tsx b/src/libs/Navigation/DebugTabNavigator.tsx new file mode 100644 index 0000000000000..3b705596cb9b6 --- /dev/null +++ b/src/libs/Navigation/DebugTabNavigator.tsx @@ -0,0 +1,152 @@ +import type {EventMapCore, NavigationProp, NavigationState} from '@react-navigation/native'; +import {useNavigation} from '@react-navigation/native'; +import {createStackNavigator} from '@react-navigation/stack'; +import React, {useEffect, useMemo, useState} from 'react'; +import {View} from 'react-native'; +import * as Expensicons from '@components/Icon/Expensicons'; +import type {LocaleContextProps} from '@components/LocaleContextProvider'; +import getBackgroundColor from '@components/TabSelector/getBackground'; +import getOpacity from '@components/TabSelector/getOpacity'; +import TabSelectorItem from '@components/TabSelector/TabSelectorItem'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; +import type IconAsset from '@src/types/utils/IconAsset'; + +type IconAndTitle = { + icon: IconAsset; + title: string; +}; + +function getIconAndTitle(route: string, translate: LocaleContextProps['translate']): IconAndTitle { + switch (route) { + case CONST.DEBUG.DETAILS: + return {icon: Expensicons.Info, title: translate('debug.details')}; + case CONST.DEBUG.JSON: + return {icon: Expensicons.Eye, title: translate('debug.JSON')}; + case CONST.DEBUG.REPORT_ACTIONS: + return {icon: Expensicons.Document, title: translate('debug.reportActions')}; + case CONST.DEBUG.REPORT_ACTION_PREVIEW: + return {icon: Expensicons.Document, title: translate('debug.reportActionPreview')}; + case CONST.DEBUG.TRANSACTION_VIOLATIONS: + return {icon: Expensicons.Exclamation, title: translate('debug.violations')}; + default: + throw new Error(`Route ${route} has no icon nor title set.`); + } +} + +const StackNavigator = createStackNavigator(); + +type DebugTabNavigatorRoute = { + name: string; + component: () => React.ReactNode; +}; + +type DebugTabNavigatorRoutes = DebugTabNavigatorRoute[]; + +type DebugTabNavigatorProps = { + id: string; + routes: DebugTabNavigatorRoutes; +}; + +function DebugTabNavigator({id, routes}: DebugTabNavigatorProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + const navigation = useNavigation>>(); + const {translate} = useLocalize(); + const [currentTab, setCurrentTab] = useState(routes.at(0)?.name); + const defaultAffectedAnimatedTabs = useMemo(() => Array.from({length: routes.length}, (v, i) => i), [routes.length]); + const [affectedAnimatedTabs, setAffectedAnimatedTabs] = useState(defaultAffectedAnimatedTabs); + + 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, currentTab]); + + return ( + <> + + {routes.map((route, index) => { + const isActive = route.name === currentTab; + const activeOpacity = getOpacity({ + routesLength: routes.length, + tabIndex: index, + active: true, + affectedTabs: affectedAnimatedTabs, + position: undefined, + isActive, + }); + const inactiveOpacity = getOpacity({ + routesLength: routes.length, + tabIndex: index, + active: false, + affectedTabs: affectedAnimatedTabs, + position: undefined, + isActive, + }); + const backgroundColor = getBackgroundColor({ + routesLength: routes.length, + tabIndex: index, + affectedTabs: affectedAnimatedTabs, + theme, + position: undefined, + isActive, + }); + const {icon, title} = getIconAndTitle(route.name, translate); + + const onPress = () => { + navigation.navigate(route.name); + setCurrentTab(route.name); + }; + + return ( + + ); + })} + + { + const event = e as unknown as EventMapCore['state']; + const state = event.data.state; + const routeNames = state.routeNames; + const newSelectedTab = state.routes.at(state.routes.length - 1)?.name; + if (currentTab === newSelectedTab || (currentTab && !routeNames.includes(currentTab))) { + return; + } + setCurrentTab(newSelectedTab); + }, + }} + > + {routes.map((route) => ( + + ))} + + + ); +} + +export default DebugTabNavigator; + +export type {DebugTabNavigatorRoutes}; diff --git a/src/pages/Debug/Report/DebugReportActions.tsx b/src/pages/Debug/Report/DebugReportActions.tsx index 9368ca5116bd9..fdc2aa8b1ca84 100644 --- a/src/pages/Debug/Report/DebugReportActions.tsx +++ b/src/pages/Debug/Report/DebugReportActions.tsx @@ -1,8 +1,6 @@ import React from 'react'; -import type {ListRenderItemInfo} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; -import FlatList from '@components/FlatList'; import {PressableWithFeedback} from '@components/Pressable'; import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; @@ -28,17 +26,20 @@ function DebugReportActions({reportID}: DebugReportActionsProps) { canEvict: false, selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, canUserPerformWriteAction, true), }); - const renderItem = ({item}: ListRenderItemInfo) => ( + + const renderItem = (item: ReportAction, index: number) => ( Navigation.navigate(ROUTES.DEBUG_REPORT_ACTION.getRoute(reportID, item.reportActionID))} style={({pressed}) => [styles.flexRow, styles.justifyContentBetween, pressed && styles.hoveredComponentBG, styles.p4]} hoverStyle={styles.hoveredComponentBG} + key={index} > {item.reportActionID} {datetimeToCalendarTime(item.created, false, false)} ); + return (