diff --git a/android/src/main/java/com/rcttabview/RCTTabView.kt b/android/src/main/java/com/rcttabview/RCTTabView.kt
index a8ee0468..79897c96 100644
--- a/android/src/main/java/com/rcttabview/RCTTabView.kt
+++ b/android/src/main/java/com/rcttabview/RCTTabView.kt
@@ -92,6 +92,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
this.items = items
items.forEachIndexed { index, item ->
val menuItem = getOrCreateItem(index, item.title)
+ menuItem.isVisible = !item.hidden
if (icons.containsKey(index)) {
menuItem.icon = getDrawable(icons[index]!!)
}
diff --git a/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt b/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt
index 82510a4c..2ecdcec2 100644
--- a/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt
+++ b/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt
@@ -12,7 +12,8 @@ data class TabInfo(
val key: String,
val title: String,
val badge: String,
- val activeTintColor: Int?
+ val activeTintColor: Int?,
+ val hidden: Boolean,
)
class RCTTabViewImpl {
@@ -29,7 +30,8 @@ class RCTTabViewImpl {
key = item.getString("key") ?: "",
title = item.getString("title") ?: "",
badge = item.getString("badge") ?: "",
- activeTintColor = if (item.hasKey("activeTintColor")) item.getInt("activeTintColor") else null
+ activeTintColor = if (item.hasKey("activeTintColor")) item.getInt("activeTintColor") else null,
+ hidden = if (item.hasKey("hidden")) item.getBoolean("hidden") else false
)
)
}
diff --git a/docs/docs/docs/guides/standalone-usage.md b/docs/docs/docs/guides/standalone-usage.md
index 9b077f01..e6973c81 100644
--- a/docs/docs/docs/guides/standalone-usage.md
+++ b/docs/docs/docs/guides/standalone-usage.md
@@ -200,4 +200,12 @@ Function to get the active tint color for a tab.
#### `getIcon`
Function to get the icon for a tab.
+
- Default: Uses `route.focusedIcon` and `route.unfocusedIcon`
+
+
+#### `getHidden`
+
+Function to determine if a tab should be hidden.
+
+- Default: Uses `route.hidden`
diff --git a/docs/docs/docs/guides/usage-with-react-navigation.mdx b/docs/docs/docs/guides/usage-with-react-navigation.mdx
index a2cdf65d..22dce85a 100644
--- a/docs/docs/docs/guides/usage-with-react-navigation.mdx
+++ b/docs/docs/docs/guides/usage-with-react-navigation.mdx
@@ -193,6 +193,16 @@ SF Symbols are only supported on Apple platforms.
Badge to show on the tab icon.
+#### `tabBarItemHidden`
+
+Whether the tab bar item is hidden.
+
+:::warning
+
+Due to native limitations on iOS, this option doesn't hide the tab item **when hidden route is focused**.
+
+:::
+
#### `lazy`
Whether this screens should render the first time it's accessed. Defaults to true. Set it to false if you want to render the screen on initial render.
diff --git a/example/src/App.tsx b/example/src/App.tsx
index 9de0519c..21db84d2 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -37,6 +37,10 @@ const FourTabsIgnoreSafeArea = () => {
return ;
};
+const HiddenTab = () => {
+ return ;
+};
+
const FourTabsRippleColor = () => {
return ;
};
@@ -114,6 +118,10 @@ const examples = [
component: FourTabsActiveIndicatorColor,
name: 'Four Tabs - Active Indicator color',
},
+ {
+ component: HiddenTab,
+ name: 'Four Tabs - With Hidden Tab',
+ },
{
component: NativeBottomTabsVectorIcons,
name: 'Native Bottom Tabs with Vector Icons',
diff --git a/example/src/Examples/FourTabs.tsx b/example/src/Examples/FourTabs.tsx
index abd92e86..a9be0675 100644
--- a/example/src/Examples/FourTabs.tsx
+++ b/example/src/Examples/FourTabs.tsx
@@ -12,6 +12,7 @@ interface Props {
scrollEdgeAppearance?: 'default' | 'opaque' | 'transparent';
barTintColor?: ColorValue;
translucent?: boolean;
+ hideOneTab?: boolean;
rippleColor?: ColorValue;
activeIndicatorColor?: ColorValue;
}
@@ -22,6 +23,7 @@ export default function FourTabs({
scrollEdgeAppearance = 'default',
barTintColor,
translucent = true,
+ hideOneTab = false,
rippleColor,
activeIndicatorColor,
}: Props) {
@@ -39,6 +41,7 @@ export default function FourTabs({
title: 'Albums',
focusedIcon: require('../../assets/icons/grid_dark.png'),
badge: '5',
+ hidden: hideOneTab,
},
{
key: 'contacts',
diff --git a/example/src/Examples/NativeBottomTabsVectorIcons.tsx b/example/src/Examples/NativeBottomTabsVectorIcons.tsx
index eca794dd..b1436c86 100644
--- a/example/src/Examples/NativeBottomTabsVectorIcons.tsx
+++ b/example/src/Examples/NativeBottomTabsVectorIcons.tsx
@@ -69,6 +69,7 @@ function NativeBottomTabsVectorIcons() {
options={{
tabBarIcon: () => messageIcon,
tabBarActiveTintColor: 'white',
+ tabBarItemHidden: true,
}}
/>
diff --git a/ios/Fabric/RCTTabViewComponentView.mm b/ios/Fabric/RCTTabViewComponentView.mm
index 17a743ce..d735a724 100644
--- a/ios/Fabric/RCTTabViewComponentView.mm
+++ b/ios/Fabric/RCTTabViewComponentView.mm
@@ -155,7 +155,8 @@ bool areTabItemsEqual(const RNCTabViewItemsStruct& lhs, const RNCTabViewItemsStr
lhs.title == rhs.title &&
lhs.sfSymbol == rhs.sfSymbol &&
lhs.badge == rhs.badge &&
- lhs.activeTintColor == rhs.activeTintColor;
+ lhs.activeTintColor == rhs.activeTintColor &&
+ lhs.hidden == rhs.hidden;
}
bool haveTabItemsChanged(const std::vector& oldItems,
@@ -178,7 +179,7 @@ bool haveTabItemsChanged(const std::vector& oldItems,
NSMutableArray *result = [NSMutableArray array];
for (const auto& item : items) {
- auto tabInfo = [[TabInfo alloc] initWithKey:RCTNSStringFromString(item.key) title:RCTNSStringFromString(item.title) badge:RCTNSStringFromString(item.badge) sfSymbol:RCTNSStringFromString(item.sfSymbol) activeTintColor:RCTUIColorFromSharedColor(item.activeTintColor)];
+ auto tabInfo = [[TabInfo alloc] initWithKey:RCTNSStringFromString(item.key) title:RCTNSStringFromString(item.title) badge:RCTNSStringFromString(item.badge) sfSymbol:RCTNSStringFromString(item.sfSymbol) activeTintColor:RCTUIColorFromSharedColor(item.activeTintColor) hidden:item.hidden];
[result addObject:tabInfo];
}
diff --git a/ios/TabViewImpl.swift b/ios/TabViewImpl.swift
index de8b8447..31225921 100644
--- a/ios/TabViewImpl.swift
+++ b/ios/TabViewImpl.swift
@@ -56,50 +56,20 @@ struct TabViewImpl: View {
var body: some View {
TabView(selection: $props.selectedPage) {
ForEach(props.children.indices, id: \.self) { index in
- let child = props.children[safe: index] ?? UIView()
- let tabData = props.items[safe: index]
- let icon = props.icons[index]
-
- RepresentableView(view: child)
- .ignoresTopSafeArea(
- props.ignoresTopSafeArea ?? false,
- frame: child.frame
- )
- .tabItem {
- TabItem(
- title: tabData?.title,
- icon: icon,
- sfSymbol: tabData?.sfSymbol,
- labeled: props.labeled
- )
- }
- .tag(tabData?.key)
- .tabBadge(tabData?.badge)
-#if os(iOS)
- .onAppear {
- // SwiftUI doesn't change `selection` when clicked on items from the "More" list.
- // More tab is visible only if we have more than 5 items.
- // This is a workaround that fixes the "More" feature.
- if index < 4 {
- return
- }
- if let key = tabData?.key, props.selectedPage != key {
- onSelect(key)
- }
- }
-#endif
+ renderTabItem(at: index)
}
-
}
.onTabItemEvent({ index, isLongPress in
- if let key = props.items[safe: index]?.key {
- if isLongPress {
- onLongPress(key)
- emitHapticFeedback(longPress: true)
- } else {
- onSelect(key)
- emitHapticFeedback()
- }
+ guard let key = props.items.filter({
+ !$0.hidden || $0.key == props.selectedPage
+ })[safe: index]?.key else { return }
+
+ if isLongPress {
+ onLongPress(key)
+ emitHapticFeedback(longPress: true)
+ } else {
+ onSelect(key)
+ emitHapticFeedback()
}
})
.tintColor(props.selectedActiveTintColor)
@@ -115,6 +85,42 @@ struct TabViewImpl: View {
}
}
+ @ViewBuilder
+ private func renderTabItem(at index: Int) -> some View {
+ let tabData = props.items[safe: index]
+ let isHidden = tabData?.hidden ?? false
+ let isFocused = props.selectedPage == tabData?.key
+
+ if !isHidden || isFocused {
+ let child = props.children[safe: index] ?? UIView()
+ let icon = props.icons[index]
+
+ RepresentableView(view: child)
+ .ignoresTopSafeArea(
+ props.ignoresTopSafeArea ?? false,
+ frame: child.frame
+ )
+ .tabItem {
+ TabItem(
+ title: tabData?.title,
+ icon: icon,
+ sfSymbol: tabData?.sfSymbol,
+ labeled: props.labeled
+ )
+ }
+ .tag(tabData?.key)
+ .tabBadge(tabData?.badge)
+#if os(iOS)
+ .onAppear {
+ guard index >= 4,
+ let key = tabData?.key,
+ props.selectedPage != key else { return }
+ onSelect(key)
+ }
+#endif
+ }
+ }
+
func emitHapticFeedback(longPress: Bool = false) {
#if os(iOS)
if !props.hapticFeedbackEnabled {
diff --git a/ios/TabViewProvider.swift b/ios/TabViewProvider.swift
index f0e74df5..1e65f442 100644
--- a/ios/TabViewProvider.swift
+++ b/ios/TabViewProvider.swift
@@ -8,6 +8,7 @@ import React
@objc public let badge: String
@objc public let sfSymbol: String
@objc public let activeTintColor: UIColor?
+ @objc public let hidden: Bool
@objc
public init(
@@ -15,13 +16,15 @@ import React
title: String,
badge: String,
sfSymbol: String,
- activeTintColor: UIColor?
+ activeTintColor: UIColor?,
+ hidden: Bool
) {
self.key = key
self.title = title
self.badge = badge
self.sfSymbol = sfSymbol
self.activeTintColor = activeTintColor
+ self.hidden = hidden
super.init()
}
}
@@ -210,7 +213,8 @@ import React
title: itemDict["title"] as? String ?? "",
badge: itemDict["badge"] as? String ?? "",
sfSymbol: itemDict["sfSymbol"] as? String ?? "",
- activeTintColor: RCTConvert.uiColor(itemDict["activeTintColor"] as? NSNumber)
+ activeTintColor: RCTConvert.uiColor(itemDict["activeTintColor"] as? NSNumber),
+ hidden: itemDict["hidden"] as? Bool ?? false
)
)
}
diff --git a/src/TabView.tsx b/src/TabView.tsx
index 954bc120..e9c5eb9e 100644
--- a/src/TabView.tsx
+++ b/src/TabView.tsx
@@ -104,6 +104,12 @@ interface Props {
focused: boolean;
}) => ImageSource | undefined;
+ /**
+ * Get hidden for the tab, uses `route.hidden` by default.
+ * If `true`, the tab will be hidden.
+ */
+ getHidden?: (props: { route: Route }) => boolean | undefined;
+
/**
* Background color of the tab bar.
*/
@@ -126,6 +132,10 @@ const TabView = ({
renderScene,
onIndexChange,
onTabLongPress,
+ getBadge,
+ rippleColor,
+ tabBarActiveTintColor: activeTintColor,
+ tabBarInactiveTintColor: inactiveTintColor,
getLazy = ({ route }: { route: Route }) => route.lazy,
getLabelText = ({ route }: { route: Route }) => route.title,
getIcon = ({ route, focused }: { route: Route; focused: boolean }) =>
@@ -135,11 +145,9 @@ const TabView = ({
: route.unfocusedIcon
: route.focusedIcon,
barTintColor,
+ getHidden = ({ route }: { route: Route }) => route.hidden,
getActiveTintColor = ({ route }: { route: Route }) => route.activeTintColor,
- tabBarActiveTintColor: activeTintColor,
- tabBarInactiveTintColor: inactiveTintColor,
hapticFeedbackEnabled = true,
- rippleColor,
...props
}: Props) => {
// @ts-ignore
@@ -190,15 +198,24 @@ const TabView = ({
'SF Symbols are not supported on Android. Use require() or pass uri to load an image instead.'
);
}
+
return {
key: route.key,
title: getLabelText({ route }) ?? route.key,
sfSymbol: isSfSymbol ? icon.sfSymbol : undefined,
- badge: props.getBadge?.({ route }),
+ badge: getBadge?.({ route }),
activeTintColor: processColor(getActiveTintColor({ route })),
+ hidden: getHidden?.({ route }),
};
}),
- [trimmedRoutes, icons, getLabelText, props, getActiveTintColor]
+ [
+ trimmedRoutes,
+ icons,
+ getLabelText,
+ getBadge,
+ getActiveTintColor,
+ getHidden,
+ ]
);
const resolvedIconAssets: ImageSource[] = useMemo(
diff --git a/src/TabViewNativeComponent.ts b/src/TabViewNativeComponent.ts
index 54908465..3da589be 100644
--- a/src/TabViewNativeComponent.ts
+++ b/src/TabViewNativeComponent.ts
@@ -14,6 +14,7 @@ export type TabViewItems = ReadonlyArray<{
sfSymbol?: string;
badge?: string;
activeTintColor?: ProcessedColorValue | null;
+ hidden?: boolean;
}>;
export interface TabViewProps extends ViewProps {
diff --git a/src/react-navigation/types.ts b/src/react-navigation/types.ts
index 0dc21948..2318ac4a 100644
--- a/src/react-navigation/types.ts
+++ b/src/react-navigation/types.ts
@@ -67,6 +67,17 @@ export type NativeBottomTabNavigationOptions = {
*/
tabBarIcon?: (props: { focused: boolean }) => ImageSourcePropType | AppleIcon;
+ /**
+ * Whether the tab bar item is visible when this screen is active.
+ * Used for compatibility with JS Tabs. Prefer using `tabBarItemHidden` as this API may be removed in the future.
+ */
+ tabBarButton?: () => null;
+
+ /**
+ * Whether the tab bar item is visible. Defaults to true.
+ */
+ tabBarItemHidden?: boolean;
+
/**
* Badge to show on the tab icon.
*/
diff --git a/src/react-navigation/views/NativeBottomTabView.tsx b/src/react-navigation/views/NativeBottomTabView.tsx
index 997be9c4..f7f504ba 100644
--- a/src/react-navigation/views/NativeBottomTabView.tsx
+++ b/src/react-navigation/views/NativeBottomTabView.tsx
@@ -40,6 +40,14 @@ export default function NativeBottomTabView({
: (route as Route).name;
}}
getBadge={({ route }) => descriptors[route.key]?.options.tabBarBadge}
+ getHidden={({ route }) => {
+ const options = descriptors[route.key]?.options;
+
+ return (
+ options?.tabBarItemHidden === true ||
+ options?.tabBarButton?.() === null
+ );
+ }}
getIcon={({ route, focused }) => {
const options = descriptors[route.key]?.options;
diff --git a/src/types.ts b/src/types.ts
index 5ddf5aaf..8307f910 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -13,6 +13,7 @@ export type BaseRoute = {
focusedIcon?: ImageSourcePropType | AppleIcon;
unfocusedIcon?: ImageSourcePropType | AppleIcon;
activeTintColor?: string;
+ hidden?: boolean;
};
export type NavigationState = {