Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 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
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 @@ -6033,14 +6033,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.

23 changes: 19 additions & 4 deletions src/components/TabSelector/TabLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React from 'react';
// eslint-disable-next-line no-restricted-imports
import {Animated, StyleSheet, View} from 'react-native';
import type {StyleProp, TextStyle} from 'react-native';
import Text from '@components/Text';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';

type TabLabelProps = {
/** Title of the tab */
Expand All @@ -16,17 +18,30 @@ type TabLabelProps = {

/** Whether an icon is present - used to conditionally apply margin */
hasIcon?: boolean;

/** Text style */
textStyle?: StyleProp<TextStyle>;
};

function TabLabel({title = '', activeOpacity = 0, inactiveOpacity = 1, hasIcon = false}: TabLabelProps) {
function TabLabel({title = '', activeOpacity = 0, inactiveOpacity = 1, hasIcon = false, textStyle = false}: TabLabelProps) {
const styles = useThemeStyles();
return (
<View>
<View style={{maxWidth: variables.tabSelectorMaxTabLabelWidth}}>
<Animated.View style={[{opacity: activeOpacity}]}>
<Text style={styles.tabText(true, hasIcon)}>{title}</Text>
<Text
numberOfLines={1}
style={[styles.tabText(true, hasIcon), textStyle]}
>
{title}
</Text>
</Animated.View>
<Animated.View style={[StyleSheet.absoluteFill, {opacity: inactiveOpacity}]}>
<Text style={styles.tabText(false, hasIcon)}>{title}</Text>
<Text
numberOfLines={1}
style={[styles.tabText(false, hasIcon), textStyle]}
>
{title}
</Text>
</Animated.View>
</View>
);
Expand Down
2 changes: 2 additions & 0 deletions src/components/TabSelector/TabSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ function TabSelector({
state,
navigation,
onTabPress = () => {},
onLongTabPress,
position,
onFocusTrapContainerElementChanged,
shouldShowLabelWhenInactive = true,
Expand Down Expand Up @@ -66,6 +67,7 @@ function TabSelector({
tabs={tabs}
activeTabKey={activeRouteName}
onTabPress={handleTabPress}
onLongTabPress={onLongTabPress}
position={position}
shouldShowLabelWhenInactive={shouldShowLabelWhenInactive}
shouldShowProductTrainingTooltip={shouldShowProductTrainingTooltip}
Expand Down
9 changes: 8 additions & 1 deletion src/components/TabSelector/TabSelectorBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ function TabSelectorBase({
tabs,
activeTabKey,
onTabPress = () => {},
onLongTabPress,
onActiveTabPress = () => {},
position,
shouldShowLabelWhenInactive = true,
equalWidth = false,
Expand Down Expand Up @@ -64,6 +66,7 @@ function TabSelectorBase({
contentContainerStyle={styles.tabSelectorContentContainer}
horizontal
showsHorizontalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
{tabs.map((tab, index) => {
const isActive = index === activeIndex;
Expand Down Expand Up @@ -94,9 +97,9 @@ function TabSelectorBase({

const handlePress = () => {
if (isActive) {
onActiveTabPress(tab.key);
return;
}

setAffectedAnimatedTabs([activeIndex, index]);
onTabPress(tab.key);
};
Expand All @@ -108,6 +111,7 @@ function TabSelectorBase({
icon={tab.icon}
title={tab.title}
onPress={handlePress}
onLongPress={onLongTabPress ? () => onLongTabPress(tab.key) : undefined}
activeOpacity={activeOpacity}
inactiveOpacity={inactiveOpacity}
backgroundColor={backgroundColor}
Expand All @@ -118,6 +122,9 @@ function TabSelectorBase({
shouldShowProductTrainingTooltip={shouldShowProductTrainingTooltip}
renderProductTrainingTooltip={renderProductTrainingTooltip}
equalWidth={equalWidth}
badgeText={tab.badgeText}
pendingAction={tab.pendingAction}
disabled={tab.disabled}
/>
);
})}
Expand Down
25 changes: 24 additions & 1 deletion src/components/TabSelector/TabSelectorContext.tsx
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 TabSelectorContextProvider({children, activeTabKey}: TabSelectorContext
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 lastScrolledToTab = useRef('');

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

lastScrolledToTab.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 (!lastScrolledToTab.current || activeTabKey === lastScrolledToTab.current) {
return;
}

lastScrolledToTab.current = activeTabKey;

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 = {
Expand Down
37 changes: 31 additions & 6 deletions src/components/TabSelector/TabSelectorItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import React, {useContext, 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';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import CONST from '@src/CONST';
Expand All @@ -22,6 +24,7 @@ function TabSelectorItem({
icon,
title = '',
onPress = () => {},
onLongPress,
backgroundColor = '',
activeOpacity = 0,
inactiveOpacity = 1,
Expand All @@ -32,7 +35,12 @@ function TabSelectorItem({
shouldShowProductTrainingTooltip = false,
renderProductTrainingTooltip,
equalWidth = false,
badgeText,
disabled = false,
pendingAction,
}: TabSelectorItemProps) {
const {isOffline} = useNetwork();

const styles = useThemeStyles();
const [isHovered, setIsHovered] = useState(false);
const shouldShowEducationalTooltip = shouldShowProductTrainingTooltip && isActive;
Expand All @@ -41,14 +49,23 @@ function TabSelectorItem({

const accessibilityState = {selected: isActive};

const isOfflinePendingAction = !!isOffline && !!pendingAction;
const needsStrikeThrough = isOffline && pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;

const children = (
<AnimatedPressableWithFeedback
ref={(ref: HTMLDivElement | View | null) => registerTab(tabKey, ref)}
accessibilityLabel={title}
accessibilityState={accessibilityState}
accessibilityRole={CONST.ROLE.TAB}
style={[styles.tabSelectorButton, styles.tabBackground(isHovered, isActive, backgroundColor), styles.userSelectNone]}
wrapperStyle={[equalWidth ? styles.flex1 : styles.flexGrow1]}
style={[
styles.tabSelectorButton,
styles.tabBackground(isHovered, isActive, disabled, backgroundColor),
styles.userSelectNone,
isOfflinePendingAction && styles.offlineFeedbackPending,
]}
wrapperStyle={equalWidth ? styles.flex1 : styles.flexGrow1}
onLongPress={onLongPress}
onPress={() => {
scrollToTab(tabKey);
onPress();
Expand All @@ -60,20 +77,28 @@ function TabSelectorItem({
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
testID={testID}
sentryLabel={sentryLabel}
disabled={disabled}
>
<TabIcon
icon={icon}
activeOpacity={styles.tabOpacity(isHovered, isActive, activeOpacity, inactiveOpacity).opacity}
inactiveOpacity={styles.tabOpacity(isHovered, isActive, inactiveOpacity, activeOpacity).opacity}
activeOpacity={styles.tabOpacity(disabled, isHovered, isActive, activeOpacity, inactiveOpacity).opacity}
inactiveOpacity={styles.tabOpacity(disabled, isHovered, isActive, inactiveOpacity, activeOpacity).opacity}
/>
{(shouldShowLabelWhenInactive || isActive) && (
<TabLabel
textStyle={needsStrikeThrough && styles.offlineFeedbackDeleted}
title={title}
activeOpacity={styles.tabOpacity(isHovered, isActive, activeOpacity, inactiveOpacity).opacity}
inactiveOpacity={styles.tabOpacity(isHovered, isActive, inactiveOpacity, activeOpacity).opacity}
activeOpacity={styles.tabOpacity(disabled, isHovered, isActive, activeOpacity, inactiveOpacity).opacity}
inactiveOpacity={styles.tabOpacity(disabled, isHovered, isActive, inactiveOpacity, activeOpacity).opacity}
hasIcon={!!icon}
/>
)}
{!!badgeText && (
<Badge
text={badgeText}
success
/>
)}
</AnimatedPressableWithFeedback>
);

Expand Down
Loading
Loading