Skip to content
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
cab3fa9
Scrollable tabs on Reports page
GCyganek Feb 19, 2026
bdbd660
Refactor SET_HISTORY_PARAM to PUSH_PARAMS for custom history navigation
adamgrzybowski Feb 24, 2026
e5781d5
Use PUSH_PARAMS navigation action in SearchPageTabSelector
adamgrzybowski Feb 24, 2026
0ace013
Remove unnecessary affectedTabs check from TabSelector background and…
adamgrzybowski Feb 24, 2026
9814fbd
Fix goBack with custom history on android
adamgrzybowski Mar 2, 2026
24f29fb
Refactor router extensions functionality
adamgrzybowski Mar 3, 2026
bd2122d
Fix not scrolling to tabs that are not in view when user navigates back
GCyganek Mar 3, 2026
0434e91
Delete SearchTypeMenuPopover and correct savedSearches processing
GCyganek Mar 3, 2026
b62c511
default export error
GCyganek Mar 3, 2026
3f92e0b
Merge branch 'main' into @GCyganek/scrollable-tabs-reports-page
GCyganek Mar 3, 2026
816df32
ExpensifyCard icon
GCyganek Mar 3, 2026
37dcfe9
Do not run useEffect on first render
GCyganek Mar 3, 2026
5a4a8a9
Improve types for router extensions
adamgrzybowski Mar 3, 2026
b8f1791
Use consts in router extensions
adamgrzybowski Mar 3, 2026
0ee328c
Fix problem with rehydration of split navigator
adamgrzybowski Mar 3, 2026
8c92c71
Fix affectedTabs opactiy & bg
GCyganek Mar 4, 2026
8c650b3
Add todo items count
GCyganek Mar 4, 2026
2cd7bb9
Fix prettier
GCyganek Mar 4, 2026
7ae12c1
Merge branch 'main' into @GCyganek/scrollable-tabs-reports-page
GCyganek Mar 4, 2026
a1a4b3e
Fix scrolling problems, add bg
GCyganek Mar 4, 2026
173b3be
Fix prettier
GCyganek Mar 4, 2026
097f8d8
Re-run jest tests
GCyganek Mar 4, 2026
8fd60f8
Merge branch 'main' into @GCyganek/scrollable-tabs-reports-page
GCyganek Mar 5, 2026
b3c5a3a
Merge branch 'main' into @GCyganek/scrollable-tabs-reports-page
GCyganek Mar 6, 2026
832145d
Update badges
GCyganek Mar 6, 2026
be008e0
Clear rawQuery when pushing tab params to avoid stale filters
adamgrzybowski Mar 6, 2026
95bc7fe
Re-run jest tests
GCyganek Mar 6, 2026
2a72117
Disable search router list on tab press
GCyganek Mar 10, 2026
8ed9b6c
Merge branch 'main' into @GCyganek/scrollable-tabs-reports-page
GCyganek Mar 10, 2026
4c31279
Fix double tap needed to select tab with focused input
GCyganek Mar 10, 2026
42f59dc
Add offline pending action tab style
GCyganek Mar 10, 2026
3c1ba10
Force onTabPress when active
GCyganek Mar 10, 2026
ea3ba6e
Fix lastPressedTab logic
GCyganek Mar 11, 2026
f4fc5dd
Delete console.log
GCyganek Mar 11, 2026
80e3dc8
Fix too long tabs by setting 256px max tab label width
GCyganek Mar 11, 2026
a535173
Saved searches titles fix
GCyganek Mar 11, 2026
3b28627
Delete console.log
GCyganek Mar 11, 2026
6f74c39
Add tab long press handler to display rename/delete popover
GCyganek Mar 11, 2026
913e1ad
Re-run jest tests
GCyganek Mar 11, 2026
8e31abc
Merge branch 'main' into @GCyganek/scrollable-tabs-reports-page
GCyganek Mar 11, 2026
858f584
Merge branch 'main' into @GCyganek/scrollable-tabs-reports-page
GCyganek Mar 11, 2026
166ccbd
Fix tabs order
GCyganek Mar 11, 2026
0f85ee1
Merge branch 'main' into @GCyganek/scrollable-tabs-reports-page
GCyganek Mar 12, 2026
f0198cd
replace forceOnTabPressWhenActive with onActiveTabPress
GCyganek Mar 12, 2026
d6b2135
Fix tab opactiy when pendingAction set
GCyganek Mar 12, 2026
43badb8
Merge branch 'main' into @GCyganek/scrollable-tabs-reports-page
GCyganek Mar 12, 2026
3fc056c
restore keyboardShouldPersistsTabs - handled
GCyganek Mar 13, 2026
f61c410
Merge branch 'main' into @GCyganek/scrollable-tabs-reports-page
GCyganek Mar 13, 2026
00c9726
Re-run jest tests
GCyganek Mar 13, 2026
c3d123c
Re-run jest tests
GCyganek Mar 13, 2026
001af80
Merge branch 'main' into @GCyganek/scrollable-tabs-reports-page
GCyganek Mar 13, 2026
3d82163
Merge branch 'main' into @GCyganek/scrollable-tabs-reports-page
GCyganek Mar 16, 2026
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
4 changes: 3 additions & 1 deletion src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5945,14 +5945,16 @@ const CONST = {
NAVIGATE: 'NAVIGATE',
SET_PARAMS: 'SET_PARAMS',
PRELOAD: 'PRELOAD',
POP: 'POP',
POP_TO: 'POP_TO',
GO_BACK: 'GO_BACK',
RESET: 'RESET',

/** These action types are custom for RootNavigator */
DISMISS_MODAL: 'DISMISS_MODAL',
OPEN_WORKSPACE_SPLIT: 'OPEN_WORKSPACE_SPLIT',
OPEN_DOMAIN_SPLIT: 'OPEN_DOMAIN_SPLIT',
SET_HISTORY_PARAM: 'SET_HISTORY_PARAM',
PUSH_PARAMS: 'PUSH_PARAMS',
REPLACE_PARAMS: 'REPLACE_PARAMS',
TOGGLE_SIDE_PANEL_WITH_HISTORY: 'TOGGLE_SIDE_PANEL_WITH_HISTORY',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import KeyboardUtils from '@src/utils/keyboard';
import SearchTypeMenuPopover from './SearchTypeMenuPopover';

// When counting absolute positioning, we need to account for borders
const BORDER_WIDTH = 1;
Expand All @@ -54,7 +53,6 @@ type SearchPageHeaderInputProps = {
};

function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRouterList, onSearchRouterFocus, handleSearch}: SearchPageHeaderInputProps) {
const [showPopupButton, setShowPopupButton] = useState(true);
const styles = useThemeStyles();
const theme = useTheme();
const {translate} = useLocalize();
Expand Down Expand Up @@ -143,17 +141,8 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo
setAutocompleteSubstitutions(substitutionsMap);
}, [allFeeds, personalAndWorkspaceCards, originalInputQuery, personalDetails, reports, taxRates, policies, currentUserAccountID, translate]);

useEffect(() => {
if (searchRouterListVisible) {
return;
}
setShowPopupButton(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchRouterListVisible]);

const onFocus = useCallback(() => {
onSearchRouterFocus?.();
setShowPopupButton(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Expand Down Expand Up @@ -289,11 +278,6 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo
onKeyPress={handleKeyPress}
/>
</Animated.View>
{showPopupButton && (
<View style={[styles.pl3]}>
<SearchTypeMenuPopover queryJSON={queryJSON} />
</View>
)}
</View>
{!!searchRouterListVisible && (
<View style={[styles.flex1]}>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, {useContext, useState} from 'react';
import React, {useContext, useEffect, useState} from 'react';
import ScrollView from '@components/ScrollView';
import getBackgroundColor from '@components/TabSelector/getBackground';
import getOpacity from '@components/TabSelector/getOpacity';
import type {TabSelectorBaseProps} from '@components/TabSelector/types';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import {ScrollableTabSelectorContext} from './ScrollableTabSelectorContext';
import ScrollableTabSelectorItem from './ScrollableTabSelectorItem';

Expand All @@ -30,6 +31,16 @@ function ScrollableTabSelectorBase({tabs, activeTabKey, onTabPress = () => {}, p

const {containerRef, onContainerLayout, onContainerScroll} = useContext(ScrollableTabSelectorContext);

// After a tab change, reset affectedAnimatedTabs once the transition is done so
// tabs settle back into the default animated state.
useEffect(() => {
const timerID = setTimeout(() => {
setAffectedAnimatedTabs(defaultAffectedAnimatedTabs);
}, CONST.ANIMATED_TRANSITION);

return () => clearTimeout(timerID);
}, [defaultAffectedAnimatedTabs, activeIndex]);

return (
<ScrollView
scrollEventThrottle={MIN_SMOOTH_SCROLL_EVENT_THROTTLE}
Expand Down Expand Up @@ -96,6 +107,7 @@ function ScrollableTabSelectorBase({tabs, activeTabKey, onTabPress = () => {}, p
sentryLabel={tab.sentryLabel}
shouldShowLabelWhenInactive={shouldShowLabelWhenInactive}
equalWidth={equalWidth}
badgeText={tab.badgeText}
/>
);
})}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {createContext, useRef} from 'react';
import React, {createContext, useEffect, useRef} from 'react';
// eslint-disable-next-line no-restricted-imports
import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, ScrollView as RNScrollView, View} from 'react-native';
import scrollToTabUtil from './scrollToTab';
Expand Down Expand Up @@ -32,6 +32,7 @@ function ScrollableTabSelectorContextProvider({children, activeTabKey}: Scrollab
const containerRef = useRef<RNScrollView>(null);
const containerLayoutRef = useRef<{x: number; width: number}>({x: 0, width: 0});
const tabsRef = useRef<Record<string, {ref: HTMLDivElement | View | null; width: number; x: number}>>({});
const lastPressedTab = useRef('');

const onContainerLayout = (event: LayoutChangeEvent) => {
const width = event.nativeEvent.layout.width;
Expand Down Expand Up @@ -68,11 +69,31 @@ function ScrollableTabSelectorContextProvider({children, activeTabKey}: Scrollab
return;
}

lastPressedTab.current = tabKey;

const {x: tabX, width: tabWidth, ref: tabRef} = tabData;

scrollToTabUtil({tabX, tabWidth, tabRef, containerRef, containerWidth: containerLayoutRef.current.width, containerX: containerLayoutRef.current.x});
};

// In case tab is not changed by tapping on a different tab we still
// want to scroll to the selected tab to make sure it's in view
useEffect(() => {
if (!lastPressedTab.current || activeTabKey === lastPressedTab.current) {
return;
}

const tabData = tabsRef.current[activeTabKey];

if (!tabData) {
return;
}

const {x: tabX, width: tabWidth, ref: tabRef} = tabData;

scrollToTabUtil({tabX, tabWidth, tabRef, containerRef, containerWidth: containerLayoutRef.current.width, containerX: containerLayoutRef.current.x});
}, [activeTabKey]);

// React Compiler auto-memoization
// eslint-disable-next-line react/jsx-no-constructed-context-values
const contextValue = {containerRef, registerTab, onTabLayout, onContainerLayout, onContainerScroll, scrollToTab};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, {useContext, useState} from 'react';
import type {View} from 'react-native';
// eslint-disable-next-line no-restricted-imports
import {Animated} from 'react-native';
import Badge from '@components/Badge';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import TabIcon from '@components/TabSelector/TabIcon';
import TabLabel from '@components/TabSelector/TabLabel';
Expand All @@ -28,6 +29,7 @@ function ScrollableTabSelectorItem({
testID,
equalWidth = false,
sentryLabel,
badgeText,
}: ScrollableTabSelectorItemProps) {
const styles = useThemeStyles();
const [isHovered, setIsHovered] = useState(false);
Expand Down Expand Up @@ -67,6 +69,12 @@ function ScrollableTabSelectorItem({
hasIcon={!!icon}
/>
)}
{!!badgeText && (
<Badge
text={badgeText}
success
/>
)}
</AnimatedPressableWithFeedback>
);
}
Expand Down
1 change: 1 addition & 0 deletions src/components/TabSelector/TabSelectorBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ function TabSelectorBase({
parentWidth={selectorWidth}
parentX={selectorX}
equalWidth={equalWidth}
badgeText={tab.badgeText}
/>
);
})}
Expand Down
8 changes: 8 additions & 0 deletions src/components/TabSelector/TabSelectorItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, {useLayoutEffect, useMemo, useRef, useState} from 'react';
// eslint-disable-next-line no-restricted-imports
import {Animated} from 'react-native';
import type {View} from 'react-native';
import Badge from '@components/Badge';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import Tooltip from '@components/Tooltip';
import EducationalTooltip from '@components/Tooltip/EducationalTooltip';
Expand Down Expand Up @@ -39,6 +40,7 @@ function TabSelectorItem({
parentX = 0,
parentWidth = 0,
equalWidth = false,
badgeText,
}: TabSelectorItemProps) {
const styles = useThemeStyles();
const [isHovered, setIsHovered] = useState(false);
Expand Down Expand Up @@ -108,6 +110,12 @@ function TabSelectorItem({
hasIcon={!!icon}
/>
)}
{!!badgeText && (
<Badge
text={badgeText}
success
/>
)}
</AnimatedPressableWithFeedback>
);

Expand Down
6 changes: 6 additions & 0 deletions src/components/TabSelector/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ type TabSelectorBaseItem = WithSentryLabel & {

/** Test identifier used to find elements in tests. */
testID?: string;

/** Text to display on the badge on the tab. */
badgeText?: string;
};

type TabSelectorBaseProps = {
Expand Down Expand Up @@ -101,6 +104,9 @@ type TabSelectorItemProps = WithSentryLabel & {

/** Function to render the content of the product training tooltip. */
renderProductTrainingTooltip?: () => React.JSX.Element;

/** Text to display on the badge on the tab. */
badgeText?: string;
};

type AnimationConfigBase = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {NavigationProp, NavigatorTypeBagBase, ParamListBase, StaticConfig, TypedNavigator} from '@react-navigation/native';
import {createNavigatorFactory} from '@react-navigation/native';
import RootNavigatorExtraContent from '@components/Navigation/RootNavigatorExtraContent';
import addSidebarRouterExtension from '@libs/Navigation/AppNavigator/routerExtensions/addSidebarRouterExtension';
import useNavigationResetOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetOnLayoutChange';
import createPlatformStackNavigatorComponent from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent';
import defaultPlatformStackScreenOptions from '@libs/Navigation/PlatformStackNavigation/defaultPlatformStackScreenOptions';
Expand All @@ -9,7 +10,7 @@ import RootStackRouter from './RootStackRouter';
import useCustomRootStackNavigatorState from './useCustomRootStackNavigatorState';

const RootStackNavigatorComponent = createPlatformStackNavigatorComponent('RootStackNavigator', {
createRouter: RootStackRouter,
createRouter: addSidebarRouterExtension(RootStackRouter),
defaultScreenOptions: defaultPlatformStackScreenOptions,
useCustomEffects: useNavigationResetOnLayoutChange,
useCustomState: useCustomRootStackNavigatorState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {NavigationProp, NavigatorTypeBagBase, ParamListBase, StaticConfig,
import {createNavigatorFactory} from '@react-navigation/native';
import SearchSidebar from '@components/Navigation/SearchSidebar';
import usePreserveNavigatorState from '@libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState';
import addPushParamsRouterExtension from '@libs/Navigation/AppNavigator/routerExtensions/addPushParamsRouterExtension';
import useNavigationResetOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetOnLayoutChange';
import createPlatformStackNavigatorComponent from '@navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent';
import defaultPlatformStackScreenOptions from '@navigation/PlatformStackNavigation/defaultPlatformStackScreenOptions';
Expand All @@ -15,7 +16,7 @@ function useCustomEffects(props: CustomEffectsHookProps) {
}

const SearchFullscreenNavigatorComponent = createPlatformStackNavigatorComponent('SearchFullscreenNavigator', {
createRouter: SearchFullscreenRouter,
createRouter: addPushParamsRouterExtension(SearchFullscreenRouter),
defaultScreenOptions: defaultPlatformStackScreenOptions,
useCustomEffects,
useCustomState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,19 @@ function shouldPopEntireNavigator(state: StackState, action: CommonActions.Actio

function SplitRouter(options: SplitNavigatorRouterOptions) {
const stackRouter = StackRouter(options);

const getRehydratedState = (partialState: StackState, configOptions: RouterConfigOptions): StackNavigationState<ParamListBase> => {
const adaptedState = adaptStateIfNecessary({state: partialState, options});
return stackRouter.getRehydratedState(adaptedState, configOptions);
};

return {
...stackRouter,
getStateForAction(state: StackNavigationState<ParamListBase>, action: CommonActions.Action | StackActionType, configOptions: RouterConfigOptions) {
if (action.type === CONST.NAVIGATION.ACTION_TYPE.RESET) {
const result = stackRouter.getStateForAction(state, action, configOptions);
return result ? getRehydratedState(result, configOptions) : result;
}
if (isPushingSidebarOnCentralPane(state, action, options)) {
if (getIsNarrowLayout()) {
const newAction = StackActions.popToTop();
Expand Down Expand Up @@ -147,10 +157,7 @@ function SplitRouter(options: SplitNavigatorRouterOptions) {
return maybeAdaptedState as StackNavigationState<ParamListBase>;
},

getRehydratedState(partialState: StackState, {routeNames, routeParamList, routeGetIdList}: RouterConfigOptions): StackNavigationState<ParamListBase> {
const adaptedState = adaptStateIfNecessary({state: partialState, options});
return stackRouter.getRehydratedState(adaptedState, {routeNames, routeParamList, routeGetIdList});
},
getRehydratedState,
};
}

Expand Down
Loading
Loading