Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions patches/react-native-screens/details.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,10 @@
- Upstream PR/issue: [#4985](https://github.com/software-mansion/react-native-screens/pull/4985)
- E/App issue: https://github.com/Expensify/App/issues/104284
- PR introducing patch: [#104368](https://github.com/Expensify/App/pull/104368)

### [react-native-screens+4.28.0+007+tab-bar-item-selection-hidden.patch](react-native-screens+4.28.0+007+tab-bar-item-selection-hidden.patch)

- Reason: Adds an iOS `tabBarItemSelectionHidden` prop to `Tabs.Host` that hides the iOS 26 liquid glass selection, which UIKit offers no API for. On Android the selected label is not bold while the active indicator is transparent. Used for Account, which has no bar item with the Insights beta (React Navigation patch 005).
- Upstream PR/issue: not reported, because the iOS part hides private UIKit views by class name, which RNScreens would not take as is.
- E/App issue: [#104197](https://github.com/Expensify/App/issues/104197)
- PR introducing patch: [#104491](https://github.com/Expensify/App/pull/104491)
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
diff --git a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceApplicator.kt b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceApplicator.kt
index f7fc390..b5e82e2 100644
--- a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceApplicator.kt
+++ b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceApplicator.kt
@@ -3,6 +3,7 @@ package com.swmansion.rnscreens.gamma.tabs.appearance
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
+import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.StateListDrawable
import android.util.TypedValue
@@ -162,7 +163,7 @@ internal class TabsAppearanceApplicator(
// Bold is set here rather than with `setItemTextAppearanceActiveBoldEnabled`, which re-applies Material's text size
// to this label alone, so Material lifts the selected item by the size difference between the two labels.
largeLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, largeFontSize)
- largeLabel.setTypeface(fontFamily, Typeface.BOLD)
+ largeLabel.setTypeface(fontFamily, if (tabBarAppearance?.tabBarItemActiveIndicatorColor == Color.TRANSPARENT) Typeface.NORMAL else Typeface.BOLD)
largeLabel.letterSpacing = 0f
}
}
diff --git a/node_modules/react-native-screens/ios/tabs/host/RNSTabBarController.h b/node_modules/react-native-screens/ios/tabs/host/RNSTabBarController.h
index d76842a..bc62e29 100644
--- a/node_modules/react-native-screens/ios/tabs/host/RNSTabBarController.h
+++ b/node_modules/react-native-screens/ios/tabs/host/RNSTabBarController.h
@@ -133,6 +133,8 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property (nonatomic, readwrite) BOOL rejectStaleNavigationStateUpdates;

+@property (nonatomic, readwrite) BOOL tabBarItemSelectionHidden;
+
/**
* Tell the controller that react provided tabs have changed (count / instances) & the child
* view controllers need to be updated.
diff --git a/node_modules/react-native-screens/ios/tabs/host/RNSTabBarController.mm b/node_modules/react-native-screens/ios/tabs/host/RNSTabBarController.mm
index 06c1957..bb2dcb0 100644
--- a/node_modules/react-native-screens/ios/tabs/host/RNSTabBarController.mm
+++ b/node_modules/react-native-screens/ios/tabs/host/RNSTabBarController.mm
@@ -177,6 +177,34 @@ - (void)detachFromParentContainerItem

#pragma mark - UIKit callbacks

+- (void)viewDidLayoutSubviews
+{
+ [super viewDidLayoutSubviews];
+ if (_tabBarItemSelectionHidden) {
+ [self updateTabBarItemSelectionIn:self.tabBar];
+ }
+}
+
+- (void)setTabBarItemSelectionHidden:(BOOL)tabBarItemSelectionHidden
+{
+ _tabBarItemSelectionHidden = tabBarItemSelectionHidden;
+ [self updateTabBarItemSelectionIn:self.tabBar];
+}
+
+// Private views that draw the iOS 26 liquid glass selection.
+- (void)updateTabBarItemSelectionIn:(UIView *)view
+{
+ for (UIView *subview in view.subviews) {
+ NSString *className = NSStringFromClass(subview.class);
+ if ([className hasSuffix:@"LiquidLensView"] || [className hasSuffix:@"SelectedContentView"] ||
+ [className hasSuffix:@"DestOutView"]) {
+ subview.hidden = _tabBarItemSelectionHidden;
+ } else {
+ [self updateTabBarItemSelectionIn:subview];
+ }
+ }
+}
+
- (void)didMoveToParentViewController:(UIViewController *)parent
{
[super didMoveToParentViewController:parent];
@@ -353,6 +381,11 @@ - (void)userDidRepeatViewControllerSelection:(nonnull UIViewController *)viewCon

- (void)userDidSelectViewController:(nonnull UIViewController *)viewController
{
+ // The tab the user picked shows its selection right away, without waiting for JS to turn the prop off.
+ if (_tabBarItemSelectionHidden) {
+ self.tabBarItemSelectionHidden = NO;
+ }
+
// At this moment the `UITabBarController` model is already updated.
RCTAssert(self.selectedViewController == viewController,
@"[RNScreens] Expected UIKit to update selectedViewController");
diff --git a/node_modules/react-native-screens/ios/tabs/host/RNSTabsHostComponentView.mm b/node_modules/react-native-screens/ios/tabs/host/RNSTabsHostComponentView.mm
index 60c9ab2..fa2afb9 100644
--- a/node_modules/react-native-screens/ios/tabs/host/RNSTabsHostComponentView.mm
+++ b/node_modules/react-native-screens/ios/tabs/host/RNSTabsHostComponentView.mm
@@ -254,6 +254,10 @@ - (void)updateProps:(const facebook::react::Props::Shared &)props
}
}

+ if (newComponentProps.tabBarItemSelectionHidden != oldComponentProps.tabBarItemSelectionHidden) {
+ _controller.tabBarItemSelectionHidden = newComponentProps.tabBarItemSelectionHidden;
+ }
+
if (newComponentProps.bottomAccessoryHidden != oldComponentProps.bottomAccessoryHidden) {
_bottomAccessoryHidden = newComponentProps.bottomAccessoryHidden;
#if RNS_TABS_BOTTOM_ACCESSORY_AVAILABLE
diff --git a/node_modules/react-native-screens/src/fabric/tabs/TabsHostIOSNativeComponent.ts b/node_modules/react-native-screens/src/fabric/tabs/TabsHostIOSNativeComponent.ts
index fcd0b7f..64b2a09 100644
--- a/node_modules/react-native-screens/src/fabric/tabs/TabsHostIOSNativeComponent.ts
+++ b/node_modules/react-native-screens/src/fabric/tabs/TabsHostIOSNativeComponent.ts
@@ -77,6 +77,7 @@ export interface NativeProps extends ViewProps {

// General
tabBarHidden?: CT.WithDefault<boolean, false>;
+ tabBarItemSelectionHidden?: CT.WithDefault<boolean, false>;
nativeContainerBackgroundColor?: ColorValue | undefined;
colorScheme?: CT.WithDefault<TabsHostColorScheme, 'inherit'>;

Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
diff --git a/node_modules/@react-navigation/bottom-tabs/lib/module/unstable/NativeBottomTabView.native.js b/node_modules/@react-navigation/bottom-tabs/lib/module/unstable/NativeBottomTabView.native.js
index 8f34943..5144b70 100644
--- a/node_modules/@react-navigation/bottom-tabs/lib/module/unstable/NativeBottomTabView.native.js
+++ b/node_modules/@react-navigation/bottom-tabs/lib/module/unstable/NativeBottomTabView.native.js
@@ -32,13 +32,12 @@ export function NativeBottomTabView({
fonts
} = useTheme();
const focusedRouteKey = state.routes[state.index].key;
- // A route with `tabBarItemHidden` gets no native tab item. While it is focused, it is drawn over the native host
- // and the tab bar is hidden, and the host keeps the last focused visible tab selected underneath.
+ // A route with `tabBarItemHidden` gets no native tab item. It is drawn inside the first visible tab, which the host
+ // selects with no item drawn as selected while the hidden route is focused.
const isRouteHidden = route => descriptors[route.key]?.options.tabBarItemHidden === true;
const isFocusedRouteHidden = isRouteHidden(state.routes[state.index]);
- // Derived from the history rather than remembered, so a route that became hidden since it was focused is skipped.
- const isRouteKeyVisible = key => state.routes.some(route => route.key === key && !isRouteHidden(route));
- const nativeSelectedRouteKey = isFocusedRouteHidden ? state.history.findLast(entry => isRouteKeyVisible(entry.key))?.key ?? state.routes.find(route => !isRouteHidden(route))?.key ?? focusedRouteKey : focusedRouteKey;
+ const hiddenRoutesHostKey = state.routes.find(route => !isRouteHidden(route))?.key;
+ const nativeSelectedRouteKey = isFocusedRouteHidden ? hiddenRoutesHostKey ?? focusedRouteKey : focusedRouteKey;
// Native tabs are identified by route name, which survives a route getting a new key. Keyed by route key, a re-keyed
// route replaces its native tab, and Android never removes the old one, so the bar shows the tab twice.
const getRouteForScreenKey = screenKey => state.routes.find(route => route.name === screenKey);
@@ -104,9 +103,10 @@ export function NativeBottomTabView({
target: route.key
});
}
- // While a hidden route is focused, native keeps the tab underneath it selected, so native reporting that tab is
- // not a request to leave the hidden route.
- if (actionOrigin === 'programmatic-js' || nativeSelectedRouteKey === route.key) {
+ // While a hidden route is focused, native keeps the tab it is drawn in selected. Native reporting that tab is not a
+ // request to leave the hidden route, but the user tapping it is.
+ const isUserLeavingHiddenRoute = isFocusedRouteHidden && actionOrigin === 'user';
+ if (actionOrigin === 'programmatic-js' || (nativeSelectedRouteKey === route.key && !isUserLeavingHiddenRoute)) {
dispatch({
type: 'CONFIRM_STATE',
confirmed
@@ -219,7 +219,8 @@ export function NativeBottomTabView({
onTabSelected: onTabSelected,
onTabSelectionRejected: onTabSelectionRejected,
onTabSelectionPrevented: onTabSelectionPrevented,
- tabBarHidden: shouldHideTabBar || isFocusedRouteHidden,
+ tabBarHidden: shouldHideTabBar,
+ tabBarItemSelectionHidden: isFocusedRouteHidden,
colorScheme: dark ? 'dark' : 'light',
nativeContainerStyle: {
backgroundColor: colors.background
@@ -243,6 +244,9 @@ export function NativeBottomTabView({
} = descriptors[route.key];
const isFocused = state.index === index;
const isPreloaded = state.preloadedRouteKeys.includes(route.key);
+ // Only the tab the hidden routes are drawn in is drawn unselected, so a tab the user taps natively shows as
+ // selected right away.
+ const isSelectionHidden = isFocusedRouteHidden && route.key === hiddenRoutesHostKey;
const {
title,
lazy = false,
@@ -291,7 +295,7 @@ export function NativeBottomTabView({
focused: false
})) : tabBarIcon != null ? getPlatformIcon(tabBarIcon) : undefined;
const selectedIcon = typeof tabBarIcon === 'function' ? getPlatformIcon(tabBarIcon({
- focused: true
+ focused: !isSelectionHidden
})) : undefined;
return /*#__PURE__*/_jsx(Tabs.Screen, {
onWillDisappear: () => onTransitionStart({
@@ -320,8 +324,8 @@ export function NativeBottomTabView({
},
specialEffects: {
repeatedTabSelection: {
- popToRoot: true,
- scrollToTop: true
+ popToRoot: !isSelectionHidden,
+ scrollToTop: !isSelectionHidden
}
},
android: {
@@ -332,8 +336,8 @@ export function NativeBottomTabView({
tabBarItemRippleColor: currentOptions?.tabBarRippleColor,
tabBarItemLabelVisibilityMode: currentOptions?.tabBarLabelVisibilityMode,
normal: normalTabItemAppearance,
- selected: selectedTabItemAppearance,
- tabBarItemActiveIndicatorColor: activeIndicatorColor,
+ selected: isSelectionHidden ? normalTabItemAppearance : selectedTabItemAppearance,
+ tabBarItemActiveIndicatorColor: isSelectionHidden ? 'transparent' : activeIndicatorColor,
tabBarItemActiveIndicatorEnabled: currentOptions?.tabBarActiveIndicatorEnabled,
tabBarItemTitleFontFamily: fontFamily,
tabBarItemTitleFontWeight: fontWeight,
@@ -379,7 +383,7 @@ export function NativeBottomTabView({
overrideScrollViewContentInsetAdjustmentBehavior
},
// Keyed by route key, so a re-keyed route still remounts its content inside the same native tab.
- children: /*#__PURE__*/_jsx(Deferred, {
+ children: [/*#__PURE__*/_jsx(Deferred, {
lazy: lazy,
visible: isFocused || isPreloaded,
children: /*#__PURE__*/_jsx(ScreenWithHeader, {
@@ -392,10 +396,13 @@ export function NativeBottomTabView({
children: render()
})
})
- }, route.key)
+ }, route.key), route.key === hiddenRoutesHostKey ? renderHiddenRoutes() : null]
}, route.name);
})
- }, "native-tabs-host"), ...state.routes.map((route, index) => {
+ }, "native-tabs-host")]
+ });
+ function renderHiddenRoutes() {
+ return state.routes.map((route, index) => {
if (!isRouteHidden(route) || !visitedHiddenRouteKeysRef.current.has(route.key)) {
return null;
}
@@ -421,8 +428,8 @@ export function NativeBottomTabView({
})
})
}, route.key);
- })]
- });
+ });
+ }
}
const styles = StyleSheet.create({
hiddenRoute: {
diff --git a/node_modules/@react-navigation/bottom-tabs/lib/typescript/src/unstable/types.d.ts b/node_modules/@react-navigation/bottom-tabs/lib/typescript/src/unstable/types.d.ts
index c05b04b..27f0df5 100644
--- a/node_modules/@react-navigation/bottom-tabs/lib/typescript/src/unstable/types.d.ts
+++ b/node_modules/@react-navigation/bottom-tabs/lib/typescript/src/unstable/types.d.ts
@@ -79,8 +79,8 @@ type IconIOSSfSymbol = {
export type NativeBottomTabIcon = IconIOSSfSymbol | IconImage;
export type NativeBottomTabNavigationOptions = NativeHeaderOptions & {
/**
- * Whether the tab gets no item in the native tab bar. While a hidden tab is focused, it is drawn over the
- * native tabs and the tab bar is hidden.
+ * Whether the tab gets no item in the native tab bar. While a hidden tab is focused, it is drawn inside the first
+ * visible tab and the tab bar shows no item as selected.
*/
tabBarItemHidden?: boolean;
/**
8 changes: 8 additions & 0 deletions patches/react-navigation/details.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@
- PR Introducing Patch: [#104238](https://github.com/Expensify/App/pull/104238)
- PR Updating Patch: N/A

### [@react-navigation+bottom-tabs+7.19.2+005+hidden-tab-under-tab-bar.patch](@react-navigation+bottom-tabs+7.19.2+005+hidden-tab-under-tab-bar.patch)

- Reason: Keeps the native tab bar, with no item drawn as selected, while a `tabBarItemHidden` route (patch 003) is focused. This replaces two parts of patch 003: the hidden routes are drawn inside the first visible tab instead of over the host with the tab bar hidden, and the host selects that first visible tab instead of the last visible one in the tab history. The user tapping that tab leaves the hidden route, while native reporting it on its own still confirms the state as in patch 003. The RNScreens prop that hides the selection is added by its patch 007.
- Upstream PR/issue: not reported, because it builds on patch 003, which has no upstream counterpart.
- E/App issue: [#104197](https://github.com/Expensify/App/issues/104197)
- PR Introducing Patch: [#104491](https://github.com/Expensify/App/pull/104491)
- PR Updating Patch: N/A

### Removed patches

- `@react-navigation+native+7.1.33+003+increase-history-go-popstate-fallback-timeout.patch` was dropped: upstream raised the `createMemoryHistory.go()` fallback timeout from 100ms to 1000ms in 7.3.18 (https://github.com/react-navigation/react-navigation/pull/13217), with the same Firefox rationale as our patch. Original context: [#94571](https://github.com/Expensify/App/issues/94571) / [#95980](https://github.com/Expensify/App/pull/95980), upstream issue https://github.com/react-navigation/react-navigation/issues/11145.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import NAVIGATION_TABS from '@components/Navigation/NavigationTabBar/NAVIGATION_TABS';
import type TabBarBottomContentProps from '@components/Navigation/TabBarBottomContent/types';

import useIsSettingsDrawnOverTabs from '@hooks/useIsSettingsDrawnOverTabs';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';

Expand All @@ -12,14 +8,13 @@ import type TabRootScreenWrapperProps from './types';

/**
* Material's bar is drawn over the full-height tab screen, so a narrow tab root screen reserves the bar's row under its
* content, and the ScreenWrapper adds the gesture inset the bar sits on below it. Account drawn over the tabs hides the bar.
* content, and the ScreenWrapper adds the gesture inset the bar sits on below it.
*/
function useTabRootScreenWrapperProps(selectedTab: TabBarBottomContentProps['selectedTab']): TabRootScreenWrapperProps {
function useTabRootScreenWrapperProps(): TabRootScreenWrapperProps {
const {shouldUseNarrowLayout} = useResponsiveLayout();
const isSettingsDrawnOverTabs = useIsSettingsDrawnOverTabs();
const styles = useThemeStyles();

if (!shouldUseNarrowLayout || (selectedTab === NAVIGATION_TABS.SETTINGS && isSettingsDrawnOverTabs)) {
if (!shouldUseNarrowLayout) {
return {};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import NAVIGATION_TABS from '@components/Navigation/NavigationTabBar/NAVIGATION_TABS';
import type TabBarBottomContentProps from '@components/Navigation/TabBarBottomContent/types';
import useTabBarCoveredHeight from '@components/Navigation/TabBarBottomContent/useTabBarCoveredHeight';

import useIsSettingsDrawnOverTabs from '@hooks/useIsSettingsDrawnOverTabs';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';

Expand All @@ -12,15 +9,14 @@ import type TabRootScreenWrapperProps from './types';
* On narrow layouts the content of a tab root screen runs under the translucent UITabBar, down to the bottom edge of
* the screen, so the ScreenWrapper runs edge to edge and adds no bottom padding, to the screen or to the offline
* indicator. Each tab root's list lets UIKit inset its end past the bar, and the offline indicator sits on the bar with
* its background running down to the bottom edge. Account drawn over the tabs hides the bar.
* its background running down to the bottom edge.
*/
function useTabRootScreenWrapperProps(selectedTab: TabBarBottomContentProps['selectedTab']): TabRootScreenWrapperProps {
function useTabRootScreenWrapperProps(): TabRootScreenWrapperProps {
const {shouldUseNarrowLayout} = useResponsiveLayout();
const isSettingsDrawnOverTabs = useIsSettingsDrawnOverTabs();
const styles = useThemeStyles();
const tabBarCoveredHeight = useTabBarCoveredHeight();

if (!shouldUseNarrowLayout || (selectedTab === NAVIGATION_TABS.SETTINGS && isSettingsDrawnOverTabs)) {
if (!shouldUseNarrowLayout) {
return {};
}

Expand Down
Loading
Loading