diff --git a/.changeset/calm-icons-preserve.md b/.changeset/calm-icons-preserve.md new file mode 100644 index 00000000..3e2c0ec7 --- /dev/null +++ b/.changeset/calm-icons-preserve.md @@ -0,0 +1,6 @@ +--- +'react-native-bottom-tabs': minor +'@bottom-tabs/react-navigation': minor +--- + +Add an icon rendering mode option for preserving original colors of tab icons. diff --git a/apps/example/assets/avatar-3.png b/apps/example/assets/avatar-3.png new file mode 100644 index 00000000..82ba4541 Binary files /dev/null and b/apps/example/assets/avatar-3.png differ diff --git a/apps/example/assets/avatar-4.png b/apps/example/assets/avatar-4.png new file mode 100644 index 00000000..e326ebe9 Binary files /dev/null and b/apps/example/assets/avatar-4.png differ diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock index 44495999..28dbfaf0 100644 --- a/apps/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -2884,6 +2884,6 @@ SPEC CHECKSUMS: SwiftUIIntrospect: fee9aa07293ee280373a591e1824e8ddc869ba5d Yoga: a3ed390a19db0459bd6839823a6ac6d9c6db198d -PODFILE CHECKSUM: 94c446c3bbb7f59f580c73dea5e50cf22b6ff990 +PODFILE CHECKSUM: e153d64da2cc12b425726c29ab2a7d70a1671df5 COCOAPODS: 1.16.2 diff --git a/apps/example/src/App.tsx b/apps/example/src/App.tsx index 90d7ee9f..5e70681a 100644 --- a/apps/example/src/App.tsx +++ b/apps/example/src/App.tsx @@ -43,6 +43,8 @@ import BottomAccessoryView from './Examples/BottomAccessoryView'; import TabBarHidden from './Examples/TabBarHidden'; import CustomTabBar from './Examples/CustomTabBar'; import NativeBottomTabsTabBarHidden from './Examples/NativeBottomTabsTabBarHidden'; +import OriginalIconColors from './Examples/OriginalIconColors'; +import NativeBottomTabsOriginalIcons from './Examples/NativeBottomTabsOriginalIcons'; import { useLogger } from '@react-navigation/devtools'; import LazyTabs from './Examples/LazyTabs'; import { LogBox } from 'react-native'; @@ -118,6 +120,10 @@ const examples = [ component: CustomTabBar, name: 'Custom tabBar', }, + { + component: OriginalIconColors, + name: 'Original icon colors', + }, { component: FourTabsRippleColor, name: 'Four Tabs with ripple Color', @@ -186,6 +192,10 @@ const examples = [ component: NativeBottomTabsTabBarHidden, name: 'Native Bottom Tabs with tabBarHidden', }, + { + component: NativeBottomTabsOriginalIcons, + name: 'Native Bottom Tabs with original icons', + }, { component: NativeBottomTabs, name: 'Native Bottom Tabs' }, { component: JSBottomTabs, name: 'JS Bottom Tabs' }, { diff --git a/apps/example/src/Examples/NativeBottomTabsOriginalIcons.tsx b/apps/example/src/Examples/NativeBottomTabsOriginalIcons.tsx new file mode 100644 index 00000000..fddf733b --- /dev/null +++ b/apps/example/src/Examples/NativeBottomTabsOriginalIcons.tsx @@ -0,0 +1,48 @@ +import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation'; +import { Article } from '../Screens/Article'; +import { Albums } from '../Screens/Albums'; +import { Contacts } from '../Screens/Contacts'; +import { Chat } from '../Screens/Chat'; + +const Tab = createNativeBottomTabNavigator(); + +export default function NativeBottomTabsOriginalIcons() { + return ( + + require('../../assets/avatar-4.png'), + }} + /> + require('../../assets/avatar-1.png'), + tabBarIconRenderingMode: 'original', + }} + /> + + focused + ? require('../../assets/avatar-4.png') + : require('../../assets/avatar-3.png'), + tabBarIconRenderingMode: 'original', + }} + /> + require('../../assets/avatar-4.png'), + tabBarIconRenderingMode: 'original', + }} + /> + + ); +} diff --git a/apps/example/src/Examples/OriginalIconColors.tsx b/apps/example/src/Examples/OriginalIconColors.tsx new file mode 100644 index 00000000..b67a83be --- /dev/null +++ b/apps/example/src/Examples/OriginalIconColors.tsx @@ -0,0 +1,51 @@ +import TabView, { SceneMap } from 'react-native-bottom-tabs'; +import { useState } from 'react'; +import { Article } from '../Screens/Article'; +import { Albums } from '../Screens/Albums'; +import { Contacts } from '../Screens/Contacts'; +import { Chat } from '../Screens/Chat'; + +const renderScene = SceneMap({ + tinted: Article, + avatar: Contacts, + album: Albums, + chat: Chat, +}); + +export default function OriginalIconColors() { + const [index, setIndex] = useState(0); + const [routes] = useState([ + { + key: 'tinted', + title: 'Tinted', + focusedIcon: require('../../assets/avatar-4.png'), + }, + { + key: 'avatar', + title: 'Avatar', + focusedIcon: require('../../assets/avatar-1.png'), + iconRenderingMode: 'original', + }, + { + key: 'album', + title: 'Album', + unfocusedIcon: require('../../assets/avatar-3.png'), + focusedIcon: require('../../assets/avatar-4.png'), + iconRenderingMode: 'original', + }, + { + key: 'chat', + title: 'Chat', + focusedIcon: require('../../assets/avatar-4.png'), + iconRenderingMode: 'original', + }, + ]); + + return ( + + ); +} diff --git a/apps/example/src/Examples/TintColors.tsx b/apps/example/src/Examples/TintColors.tsx index 61351eaa..62c87340 100644 --- a/apps/example/src/Examples/TintColors.tsx +++ b/apps/example/src/Examples/TintColors.tsx @@ -29,9 +29,10 @@ export default function TintColorsExample() { { key: 'albums', title: 'Albums', - focusedIcon: require('../../assets/icons/grid_dark.png'), - badge: '5', - activeTintColor: 'green', + unfocusedIcon: require('../../assets/avatar-3.png'), + focusedIcon: require('../../assets/avatar-4.png'), + activeTintColor: 'purple', + iconRenderingMode: 'original', }, { key: 'contacts', diff --git a/docs/docs/docs/guides/standalone-usage.mdx b/docs/docs/docs/guides/standalone-usage.mdx index 59a6e5c2..d9f02c43 100644 --- a/docs/docs/docs/guides/standalone-usage.mdx +++ b/docs/docs/docs/guides/standalone-usage.mdx @@ -236,6 +236,7 @@ Each route in the `routes` array can have the following properties: - `title`: Display title for the tab - `focusedIcon`: Icon to show when tab is active - `unfocusedIcon`: Icon to show when tab is inactive (optional) +- `iconRenderingMode`: Rendering mode for icons. Use `'original'` to preserve multicolor icons instead of applying the native tab tint. - `badge`: Badge text to display on the tab - `activeTintColor`: Custom active tint color for this specific tab - `lazy`: Whether to lazy load this tab's content @@ -280,6 +281,15 @@ Function to get the icon for a tab. - Default: Uses `route.focusedIcon` and `route.unfocusedIcon` +#### `getIconRenderingMode` + +Function to get the rendering mode for an image icon. + +- Default: Uses `route.iconRenderingMode` +- Options: `'automatic' | 'original'` + +Use `original` to preserve the original colors of multicolor image icons, such as logos or branded assets, instead of applying the native tab tint. + #### `getHidden` Function to determine if a tab should be hidden. diff --git a/docs/docs/docs/guides/usage-with-react-navigation.mdx b/docs/docs/docs/guides/usage-with-react-navigation.mdx index d7bda582..78e49d55 100644 --- a/docs/docs/docs/guides/usage-with-react-navigation.mdx +++ b/docs/docs/docs/guides/usage-with-react-navigation.mdx @@ -185,6 +185,7 @@ Controls how the tab bar behaves when content is scrolled. - Default: `undefined` (uses system default) Options: + - `automatic`: Platform determines the behavior - `onScrollDown`: Tab bar minimizes when scrolling down - `onScrollUp`: Tab bar minimizes when scrolling up @@ -225,15 +226,14 @@ function MyTabBar({ state, descriptors, navigation }) { function MyTabs() { return ( - } - > + }> ); } ``` + #### `renderBottomAccessoryView` Function that returns a React element to render as [bottom accessory](https://developer.apple.com/documentation/uikit/uitabbarcontroller/bottomaccessory). @@ -286,9 +286,29 @@ Function that given `{ focused: boolean }` returns `ImageSource` or `AppleIcon` SF Symbols are only supported on Apple platforms. ::: +#### `tabBarIconRenderingMode` + +Rendering mode for image icons. + +- Type: `'automatic' | 'original'` +- Default: `'automatic'` + +Use `original` to preserve the original colors of multicolor image icons, such as logos or branded assets, instead of applying the native tab tint. + +```tsx + require('brand-logo.png'), + tabBarIconRenderingMode: 'original', + }} +/> +``` + :::warning -Metro transformers that modify the import resolutions of images to something that is *not* React Native's `ImageSource` will break this. A notable community example of such is [react-native-svg-transformer](https://github.com/kristerkari/react-native-svg-transformer). +Metro transformers that modify the import resolutions of images to something that is _not_ React Native's `ImageSource` will break this. A notable community example of such is [react-native-svg-transformer](https://github.com/kristerkari/react-native-svg-transformer). For best results, avoid the use of such transformers altogether. @@ -312,13 +332,13 @@ config.resolver.sourceExts = [...config.resolver.sourceExts, "svg"]; ``` Then change your require calls like so: + ``` tabBarIcon: () => require('person.svgx') ``` ::: - #### `tabBarBadge` Badge to show on the tab icon. @@ -329,14 +349,14 @@ To display a badge without text (just a dot), you need to pass a string with a s #### `tabBarBadgeBackgroundColor` - - Type: `string` +- Type: `string` Set the background color for the badge on android. Uses the system color by default. #### `tabBarBadgeTextColor` - - Type: `string` +- Type: `string` Set the text color for the badge on android. Uses the system color by default. @@ -356,6 +376,7 @@ Due to native limitations on iOS, this option doesn't hide the tab item **when h 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. #### `freezeOnBlur` + Boolean indicating whether to prevent inactive screens from re-rendering. Defaults to false. It's working separately from `enableFreeze()` in `react-native-screens`. So settings won't be shared between them. @@ -371,7 +392,6 @@ Whether to prevent default tab switching behavior when this tab is pressed. This Due to iOS 26's new tab switching animations, controlling tab switching from JavaScript can cause significant delays. The `preventsDefault` option allows you to define this behavior statically to avoid animation delays. ::: - #### `tabBarButtonTestID` Test ID for the tab item. This can be used to find the tab item in the native view hierarchy. @@ -413,7 +433,6 @@ React.useEffect(() => { const unsubscribe = navigation.addListener('tabPress', (e) => { // Note: For iOS 26+, use the `preventsDefault` option instead of `e.preventDefault()` // to avoid animation delays - // Do something manually // ... }); diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt index 1d0c4047..911d2789 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.content.Context import android.content.res.ColorStateList import android.content.res.Configuration +import android.graphics.PorterDuff import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.os.Build @@ -19,6 +20,7 @@ import android.view.ViewGroup import android.widget.FrameLayout import android.widget.LinearLayout import android.widget.TextView +import androidx.core.view.MenuItemCompat import androidx.core.view.forEachIndexed import coil3.ImageLoader import coil3.asDrawable @@ -246,9 +248,11 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { } menuItem.isVisible = !item.hidden + updateIconTintMode(menuItem, item) if (iconSources.containsKey(index)) { getDrawable(iconSources[index]!!) { menuItem.icon = it + updateIconTintMode(menuItem, item) } } @@ -297,6 +301,13 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { return bottomNavigation.menu.findItem(index) ?: bottomNavigation.menu.add(0, index, 0, title) } + private fun updateIconTintMode(menuItem: MenuItem, item: TabInfo) { + MenuItemCompat.setIconTintMode( + menuItem, + if (item.iconRenderingMode == "original") PorterDuff.Mode.DST else null + ) + } + fun setIcons(icons: ReadableArray?) { if (icons == null || icons.size() == 0) { return @@ -316,6 +327,9 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { bottomNavigation.menu.findItem(idx)?.let { menuItem -> getDrawable(imageSource) { menuItem.icon = it + items.getOrNull(idx)?.let { item -> + updateIconTintMode(menuItem, item) + } } } } diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewManager.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewManager.kt index 95644dbb..6451db01 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewManager.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewManager.kt @@ -24,6 +24,7 @@ data class TabInfo( val badgeBackgroundColor: Int?, val badgeTextColor: Int?, val activeTintColor: Int?, + val iconRenderingMode: String?, val hidden: Boolean, val testID: String? ) @@ -104,6 +105,7 @@ class RCTTabViewManager(context: ReactApplicationContext) : badgeBackgroundColor = if (item.hasKey("badgeBackgroundColor")) item.getInt("badgeBackgroundColor") else null, badgeTextColor = if (item.hasKey("badgeTextColor")) item.getInt("badgeTextColor") else null, activeTintColor = if (item.hasKey("activeTintColor")) item.getInt("activeTintColor") else null, + iconRenderingMode = if (item.hasKey("iconRenderingMode")) item.getString("iconRenderingMode") else null, hidden = if (item.hasKey("hidden")) item.getBoolean("hidden") else false, testID = item.getString("testID") ) diff --git a/packages/react-native-bottom-tabs/ios/RCTTabViewComponentView.mm b/packages/react-native-bottom-tabs/ios/RCTTabViewComponentView.mm index bd8edcef..f25b3585 100644 --- a/packages/react-native-bottom-tabs/ios/RCTTabViewComponentView.mm +++ b/packages/react-native-bottom-tabs/ios/RCTTabViewComponentView.mm @@ -37,6 +37,7 @@ lhs.sfSymbol == rhs.sfSymbol && lhs.badge == rhs.badge && lhs.activeTintColor == rhs.activeTintColor && + lhs.iconRenderingMode == rhs.iconRenderingMode && lhs.hidden == rhs.hidden && lhs.testID == rhs.testID && lhs.role == rhs.role && @@ -197,6 +198,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & badge:RCTNSStringFromStringNilIfEmpty(item.badge) sfSymbol:RCTNSStringFromStringNilIfEmpty(item.sfSymbol) activeTintColor:RCTUIColorFromSharedColor(item.activeTintColor) + iconRenderingMode:RCTNSStringFromStringNilIfEmpty(item.iconRenderingMode) hidden:item.hidden testID:RCTNSStringFromStringNilIfEmpty(item.testID) role:RCTNSStringFromStringNilIfEmpty(item.role) @@ -265,4 +267,3 @@ - (void)onLayoutWithSize:(CGSize)size reactTag:(NSNumber *)reactTag { } #endif // RCT_NEW_ARCH_ENABLED - diff --git a/packages/react-native-bottom-tabs/ios/TabItem.swift b/packages/react-native-bottom-tabs/ios/TabItem.swift index 3866c59b..3992f27d 100644 --- a/packages/react-native-bottom-tabs/ios/TabItem.swift +++ b/packages/react-native-bottom-tabs/ios/TabItem.swift @@ -5,13 +5,14 @@ struct TabItem: View { var icon: PlatformImage? var sfSymbol: String? var labeled: Bool? + var iconRenderingMode: String? var body: some View { if let icon { #if os(macOS) Image(nsImage: icon) #else - Image(uiImage: icon) + Image(uiImage: renderedIcon(icon)) #endif } else if let sfSymbol, !sfSymbol.isEmpty { Image(systemName: sfSymbol) @@ -21,4 +22,14 @@ struct TabItem: View { Text(title ?? "") } } + +#if !os(macOS) + private var preservesOriginalIconColors: Bool { + iconRenderingMode == "original" + } + + private func renderedIcon(_ icon: UIImage) -> UIImage { + preservesOriginalIconColors ? icon.withRenderingMode(.alwaysOriginal) : icon + } +#endif } diff --git a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift index 5bfc58f4..188ddfde 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift @@ -56,7 +56,8 @@ struct LegacyTabView: AnyTabView { title: tabData.title, icon: icon, sfSymbol: tabData.sfSymbol, - labeled: props.labeled + labeled: props.labeled, + iconRenderingMode: tabData.iconRenderingMode ) .accessibilityIdentifier(tabData.testID ?? "") } diff --git a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift index 9e8dd80a..2fdc681c 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift @@ -48,7 +48,8 @@ struct NewTabView: AnyTabView { title: tabData.title, icon: icon, sfSymbol: tabData.sfSymbol, - labeled: props.labeled + labeled: props.labeled, + iconRenderingMode: tabData.iconRenderingMode ) } #if !os(tvOS) diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index 14732fdd..dee9afa4 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -229,6 +229,7 @@ struct TabViewImpl: View { let tabActiveColor = tabData.activeTintColor ?? props.activeTintColor let assetIcon = props.icons[itemIndex] let icon = assetIcon ?? makeSFSymbolImage(named: tabData.sfSymbol) + let preservesOriginalIconColors = preservesOriginalIconColors(tabData: tabData) let shouldRenderLabelIntoImage = props.hasCustomTintColors && props.labeled && tabData.role != .search && icon != nil @@ -241,12 +242,14 @@ struct TabViewImpl: View { icon: icon, title: tabData.title, color: props.inactiveTintColor, + preservesOriginalIconColors: preservesOriginalIconColors, props: props ) item.selectedImage = makeTabBarItemImage( icon: icon, title: tabData.title, color: tabActiveColor, + preservesOriginalIconColors: preservesOriginalIconColors, props: props ) continue @@ -256,14 +259,20 @@ struct TabViewImpl: View { item.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: 0) if let icon { - item.image = - props.inactiveTintColor.map { - icon.withTintColor($0, renderingMode: .alwaysOriginal) - } ?? icon - item.selectedImage = - tabActiveColor.map { - icon.withTintColor($0, renderingMode: .alwaysOriginal) - } ?? icon + if preservesOriginalIconColors { + let originalIcon = icon.withRenderingMode(.alwaysOriginal) + item.image = originalIcon + item.selectedImage = originalIcon + } else { + item.image = + props.inactiveTintColor.map { + icon.withTintColor($0, renderingMode: .alwaysOriginal) + } ?? icon + item.selectedImage = + tabActiveColor.map { + icon.withTintColor($0, renderingMode: .alwaysOriginal) + } ?? icon + } } item.setTitleTextAttributes( @@ -290,14 +299,21 @@ struct TabViewImpl: View { let assetIcon = props.icons[itemIndex] let icon = assetIcon ?? makeSFSymbolImage(named: tabData.sfSymbol) + let originalIcon = icon.map { + preservesOriginalIconColors(tabData: tabData) ? $0.withRenderingMode(.alwaysOriginal) : $0 + } item.title = props.labeled ? tabData.title : nil item.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: 0) - item.image = icon - item.selectedImage = icon + item.image = originalIcon + item.selectedImage = originalIcon } } + private func preservesOriginalIconColors(tabData: TabInfo) -> Bool { + tabData.iconRenderingMode == "original" + } + private func makeSFSymbolImage(named sfSymbol: String?) -> UIImage? { guard let sfSymbol, !sfSymbol.isEmpty else { return nil } @@ -331,6 +347,7 @@ struct TabViewImpl: View { icon: UIImage, title: String, color: UIColor?, + preservesOriginalIconColors: Bool = false, props: TabViewProps ) -> UIImage { let color = color ?? .label @@ -357,7 +374,12 @@ struct TabViewImpl: View { format.scale = UIScreen.main.scale let image = UIGraphicsImageRenderer(size: imageSize, format: format).image { _ in - let tintedIcon = icon.withTintColor(color, renderingMode: .alwaysOriginal) + let tintedIcon: UIImage + if preservesOriginalIconColors { + tintedIcon = icon.withRenderingMode(.alwaysOriginal) + } else { + tintedIcon = icon.withTintColor(color, renderingMode: .alwaysOriginal) + } let iconFrame = aspectFitRect( size: tintedIcon.size, in: CGRect( diff --git a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift index 799f3c0c..e6dd2a2e 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift @@ -9,6 +9,7 @@ public final class TabInfo: NSObject { public let badge: String? public let sfSymbol: String public let activeTintColor: PlatformColor? + public let iconRenderingMode: String? public let hidden: Bool public let testID: String? public let role: TabBarRole? @@ -20,6 +21,7 @@ public final class TabInfo: NSObject { badge: String?, sfSymbol: String, activeTintColor: PlatformColor?, + iconRenderingMode: String?, hidden: Bool, testID: String?, role: String?, @@ -30,6 +32,7 @@ public final class TabInfo: NSObject { self.badge = badge self.sfSymbol = sfSymbol self.activeTintColor = activeTintColor + self.iconRenderingMode = iconRenderingMode self.hidden = hidden self.testID = testID self.role = TabBarRole(rawValue: role ?? "") diff --git a/packages/react-native-bottom-tabs/src/TabView.tsx b/packages/react-native-bottom-tabs/src/TabView.tsx index e4f007e1..d9a1ccab 100644 --- a/packages/react-native-bottom-tabs/src/TabView.tsx +++ b/packages/react-native-bottom-tabs/src/TabView.tsx @@ -25,6 +25,7 @@ import useLatestCallback from 'use-latest-callback'; import type { AppleIcon, BaseRoute, + IconRenderingMode, LayoutDirection, NavigationState, TabRole, @@ -145,6 +146,15 @@ interface Props { focused: boolean; }) => ImageSource | AppleIcon | undefined | null; + /** + * Get the rendering mode for the tab icon, uses `route.iconRenderingMode` by default. + * + * Use `original` to preserve multicolor image icons instead of applying the native tab tint. + */ + getIconRenderingMode?: (props: { + route: Route; + }) => IconRenderingMode | undefined; + /** * Get hidden for the tab, uses `route.hidden` by default. * If `true`, the tab will be hidden. @@ -252,6 +262,8 @@ const TabView = ({ getActiveTintColor = ({ route }: { route: Route }) => route.activeTintColor, getTestID = ({ route }: { route: Route }) => route.testID, getRole = ({ route }: { route: Route }) => route.role, + getIconRenderingMode = ({ route }: { route: Route }) => + route.iconRenderingMode, getSceneStyle = ({ route }: { route: Route }) => route.style, getPreventsDefault = ({ route }: { route: Route }) => route.preventsDefault, hapticFeedbackEnabled = false, @@ -331,6 +343,7 @@ const TabView = ({ ), badgeTextColor: processColor(getBadgeTextColor?.({ route })), activeTintColor: processColor(getActiveTintColor({ route })), + iconRenderingMode: getIconRenderingMode({ route }), hidden: getHidden?.({ route }), testID: getTestID?.({ route }), role: getRole?.({ route }), @@ -345,6 +358,7 @@ const TabView = ({ getBadgeBackgroundColor, getBadgeTextColor, getActiveTintColor, + getIconRenderingMode, getHidden, getTestID, getRole, diff --git a/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts b/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts index 4a4e81ad..387ab9f4 100644 --- a/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts +++ b/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts @@ -30,6 +30,7 @@ export type TabViewItems = ReadonlyArray<{ badgeBackgroundColor?: ProcessedColorValue | null; badgeTextColor?: ProcessedColorValue | null; activeTintColor?: ProcessedColorValue | null; + iconRenderingMode?: string; hidden?: boolean; testID?: string; role?: string; diff --git a/packages/react-native-bottom-tabs/src/index.tsx b/packages/react-native-bottom-tabs/src/index.tsx index f0487bc8..19544834 100644 --- a/packages/react-native-bottom-tabs/src/index.tsx +++ b/packages/react-native-bottom-tabs/src/index.tsx @@ -15,4 +15,9 @@ export { BottomTabBarHeightContext } from './utils/BottomTabBarHeightContext'; /** * Types */ -export type { AppleIcon, LayoutDirection, TabRole } from './types'; +export type { + AppleIcon, + IconRenderingMode, + LayoutDirection, + TabRole, +} from './types'; diff --git a/packages/react-native-bottom-tabs/src/types.ts b/packages/react-native-bottom-tabs/src/types.ts index 83a9b2bb..3f4eda80 100644 --- a/packages/react-native-bottom-tabs/src/types.ts +++ b/packages/react-native-bottom-tabs/src/types.ts @@ -7,6 +7,8 @@ export type AppleIcon = { sfSymbol: SFSymbol }; export type TabRole = 'search'; +export type IconRenderingMode = 'automatic' | 'original'; + export type LayoutDirection = 'ltr' | 'rtl' | 'locale'; export type BaseRoute = { @@ -18,6 +20,7 @@ export type BaseRoute = { lazy?: boolean; focusedIcon?: ImageSourcePropType | AppleIcon; unfocusedIcon?: ImageSourcePropType | AppleIcon; + iconRenderingMode?: IconRenderingMode; activeTintColor?: string; hidden?: boolean; testID?: string; diff --git a/packages/react-navigation/src/types.ts b/packages/react-navigation/src/types.ts index b55afb05..eb1967cd 100644 --- a/packages/react-navigation/src/types.ts +++ b/packages/react-navigation/src/types.ts @@ -9,7 +9,11 @@ import type { } from '@react-navigation/native'; import type { ImageSourcePropType, StyleProp, ViewStyle } from 'react-native'; import type TabView from 'react-native-bottom-tabs'; -import type { AppleIcon, TabRole } from 'react-native-bottom-tabs'; +import type { + AppleIcon, + IconRenderingMode, + TabRole, +} from 'react-native-bottom-tabs'; export type NativeBottomTabNavigationEventMap = { /** @@ -67,6 +71,11 @@ export type NativeBottomTabNavigationOptions = { */ tabBarIcon?: (props: { focused: boolean }) => ImageSourcePropType | AppleIcon; + /** + * Rendering mode for the tab icon. Use `original` to preserve multicolor image icons. + */ + tabBarIconRenderingMode?: IconRenderingMode; + /** * Whether the tab bar item is visible. Defaults to true. */ @@ -150,6 +159,7 @@ export type NativeBottomTabNavigationConfig = Partial< | 'renderScene' | 'getLazy' | 'getIcon' + | 'getIconRenderingMode' | 'getLabelText' | 'getBadge' | 'getBadgeBackgroundColor' diff --git a/packages/react-navigation/src/views/NativeBottomTabView.tsx b/packages/react-navigation/src/views/NativeBottomTabView.tsx index 2bc2de4e..629bec4b 100644 --- a/packages/react-navigation/src/views/NativeBottomTabView.tsx +++ b/packages/react-navigation/src/views/NativeBottomTabView.tsx @@ -57,6 +57,9 @@ export default function NativeBottomTabView({ descriptors[route.key]?.options.tabBarButtonTestID } getRole={({ route }) => descriptors[route.key]?.options.role} + getIconRenderingMode={({ route }) => + descriptors[route.key]?.options.tabBarIconRenderingMode + } tabBar={ tabBar ? () => tabBar({ state, descriptors, navigation }) : undefined }