Skip to content
Merged
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
1 change: 1 addition & 0 deletions android/src/main/java/com/rcttabview/RCTTabView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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]!!)
}
Expand Down
6 changes: 4 additions & 2 deletions android/src/main/java/com/rcttabview/RCTTabViewImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
)
)
}
Expand Down
8 changes: 8 additions & 0 deletions docs/docs/docs/guides/standalone-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
10 changes: 10 additions & 0 deletions docs/docs/docs/guides/usage-with-react-navigation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ const FourTabsIgnoreSafeArea = () => {
return <FourTabs ignoresTopSafeArea />;
};

const HiddenTab = () => {
return <FourTabs hideOneTab />;
};

const FourTabsRippleColor = () => {
return <FourTabs rippleColor={'#00ff00'} />;
};
Expand Down Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions example/src/Examples/FourTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface Props {
scrollEdgeAppearance?: 'default' | 'opaque' | 'transparent';
barTintColor?: ColorValue;
translucent?: boolean;
hideOneTab?: boolean;
rippleColor?: ColorValue;
activeIndicatorColor?: ColorValue;
}
Expand All @@ -22,6 +23,7 @@ export default function FourTabs({
scrollEdgeAppearance = 'default',
barTintColor,
translucent = true,
hideOneTab = false,
rippleColor,
activeIndicatorColor,
}: Props) {
Expand All @@ -39,6 +41,7 @@ export default function FourTabs({
title: 'Albums',
focusedIcon: require('../../assets/icons/grid_dark.png'),
badge: '5',
hidden: hideOneTab,
},
{
key: 'contacts',
Expand Down
1 change: 1 addition & 0 deletions example/src/Examples/NativeBottomTabsVectorIcons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ function NativeBottomTabsVectorIcons() {
options={{
tabBarIcon: () => messageIcon,
tabBarActiveTintColor: 'white',
tabBarItemHidden: true,
}}
/>
</Tab.Navigator>
Expand Down
5 changes: 3 additions & 2 deletions ios/Fabric/RCTTabViewComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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<RNCTabViewItemsStruct>& oldItems,
Expand All @@ -178,7 +179,7 @@ bool haveTabItemsChanged(const std::vector<RNCTabViewItemsStruct>& oldItems,
NSMutableArray<TabInfo *> *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];
}
Expand Down
88 changes: 47 additions & 41 deletions ios/TabViewImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
8 changes: 6 additions & 2 deletions ios/TabViewProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,23 @@ 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(
key: String,
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()
}
}
Expand Down Expand Up @@ -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
)
)
}
Expand Down
27 changes: 22 additions & 5 deletions src/TabView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ interface Props<Route extends BaseRoute> {
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.
*/
Expand All @@ -126,6 +132,10 @@ const TabView = <Route extends BaseRoute>({
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 }) =>
Expand All @@ -135,11 +145,9 @@ const TabView = <Route extends BaseRoute>({
: 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<Route>) => {
// @ts-ignore
Expand Down Expand Up @@ -190,15 +198,24 @@ const TabView = <Route extends BaseRoute>({
'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(
Expand Down
1 change: 1 addition & 0 deletions src/TabViewNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type TabViewItems = ReadonlyArray<{
sfSymbol?: string;
badge?: string;
activeTintColor?: ProcessedColorValue | null;
hidden?: boolean;
}>;

export interface TabViewProps extends ViewProps {
Expand Down
11 changes: 11 additions & 0 deletions src/react-navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
8 changes: 8 additions & 0 deletions src/react-navigation/views/NativeBottomTabView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ export default function NativeBottomTabView({
: (route as Route<string>).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;

Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type BaseRoute = {
focusedIcon?: ImageSourcePropType | AppleIcon;
unfocusedIcon?: ImageSourcePropType | AppleIcon;
activeTintColor?: string;
hidden?: boolean;
};

export type NavigationState<Route extends BaseRoute> = {
Expand Down