diff --git a/.changeset/green-ravens-fail.md b/.changeset/green-ravens-fail.md new file mode 100644 index 00000000..07d563b3 --- /dev/null +++ b/.changeset/green-ravens-fail.md @@ -0,0 +1,6 @@ +--- +"react-native-bottom-tabs": patch +"@bottom-tabs/react-navigation": patch +--- + +feat: add support for testID diff --git a/apps/example/src/Examples/NativeBottomTabs.tsx b/apps/example/src/Examples/NativeBottomTabs.tsx index 07043b6f..23d63873 100644 --- a/apps/example/src/Examples/NativeBottomTabs.tsx +++ b/apps/example/src/Examples/NativeBottomTabs.tsx @@ -41,6 +41,7 @@ function NativeBottomTabs() { }, }} options={{ + tabBarButtonTestID: 'articleTestID', tabBarBadge: '10', tabBarIcon: ({ focused }) => focused diff --git a/apps/example/src/Examples/ThreeTabs.tsx b/apps/example/src/Examples/ThreeTabs.tsx index 8c867471..e5b44634 100644 --- a/apps/example/src/Examples/ThreeTabs.tsx +++ b/apps/example/src/Examples/ThreeTabs.tsx @@ -13,17 +13,20 @@ export default function ThreeTabs() { focusedIcon: require('../../assets/icons/article_dark.png'), unfocusedIcon: require('../../assets/icons/chat_dark.png'), badge: '!', + testID: 'articleTestID', }, { key: 'albums', title: 'Albums', focusedIcon: require('../../assets/icons/grid_dark.png'), badge: '5', + testID: 'albumsTestID', }, { key: 'contacts', focusedIcon: require('../../assets/icons/person_dark.png'), title: 'Contacts', + testID: 'contactsTestID', }, ]); diff --git a/docs/docs/docs/guides/standalone-usage.md b/docs/docs/docs/guides/standalone-usage.md index e879abed..5f6fb1bb 100644 --- a/docs/docs/docs/guides/standalone-usage.md +++ b/docs/docs/docs/guides/standalone-usage.md @@ -219,3 +219,8 @@ Function to get the icon for a tab. Function to determine if a tab should be hidden. - Default: Uses `route.hidden` + +#### `getTestID` + +Function to get the test ID for a tab item. +- Default: Uses `route.testID` diff --git a/docs/docs/docs/guides/usage-with-react-navigation.mdx b/docs/docs/docs/guides/usage-with-react-navigation.mdx index 2ee08ff8..a63d737e 100644 --- a/docs/docs/docs/guides/usage-with-react-navigation.mdx +++ b/docs/docs/docs/guides/usage-with-react-navigation.mdx @@ -162,11 +162,11 @@ Whether to enable haptic feedback on tab press. Defaults to false. Object containing styles for the tab label. Supported properties: + - `fontFamily` - `fontSize` - `fontWeight` - ### Options The following options can be used to configure the screens in the navigator. These can be specified under `screenOptions` prop of `Tab.navigator` or `options` prop of `Tab.Screen`. @@ -229,6 +229,10 @@ 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. +#### `tabBarButtonTestID` + +Test ID for the tab item. This can be used to find the tab item in the native view hierarchy. + ### Events The navigator can emit events on certain actions. Supported events are: 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 65e7b4f9..5d9ac206 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 @@ -120,13 +120,22 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context removeBadge(index) } post { - findViewById(menuItem.itemId).setOnLongClickListener { - onTabLongPressed(menuItem) - true - } - findViewById(menuItem.itemId).setOnClickListener { - onTabSelected(menuItem) - updateTintColors(menuItem) + val itemView = findViewById(menuItem.itemId) + itemView?.let { view -> + view.setOnLongClickListener { + onTabLongPressed(menuItem) + true + } + view.setOnClickListener { + onTabSelected(menuItem) + updateTintColors(menuItem) + } + + item.testID?.let { testId -> + view.findViewById(com.google.android.material.R.id.navigation_bar_item_content_container)?.apply { + tag = testId + } + } } updateTextAppearance() } diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt index 2ecdcec2..3d51039f 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt @@ -14,6 +14,7 @@ data class TabInfo( val badge: String, val activeTintColor: Int?, val hidden: Boolean, + val testID: String?, ) class RCTTabViewImpl { @@ -31,7 +32,8 @@ class RCTTabViewImpl { title = item.getString("title") ?: "", badge = item.getString("badge") ?: "", activeTintColor = if (item.hasKey("activeTintColor")) item.getInt("activeTintColor") else null, - hidden = if (item.hasKey("hidden")) item.getBoolean("hidden") else false + hidden = if (item.hasKey("hidden")) item.getBoolean("hidden") else false, + testID = item.getString("testID") ) ) } diff --git a/packages/react-native-bottom-tabs/ios/Fabric/RCTTabViewComponentView.mm b/packages/react-native-bottom-tabs/ios/Fabric/RCTTabViewComponentView.mm index 7fe7481c..424081e2 100644 --- a/packages/react-native-bottom-tabs/ios/Fabric/RCTTabViewComponentView.mm +++ b/packages/react-native-bottom-tabs/ios/Fabric/RCTTabViewComponentView.mm @@ -173,7 +173,8 @@ bool areTabItemsEqual(const RNCTabViewItemsStruct& lhs, const RNCTabViewItemsStr lhs.sfSymbol == rhs.sfSymbol && lhs.badge == rhs.badge && lhs.activeTintColor == rhs.activeTintColor && - lhs.hidden == rhs.hidden; + lhs.hidden == rhs.hidden && + lhs.testID == rhs.testID; } bool haveTabItemsChanged(const std::vector& oldItems, @@ -201,7 +202,8 @@ bool haveTabItemsChanged(const std::vector& oldItems, badge:RCTNSStringFromStringNilIfEmpty(item.badge) sfSymbol:RCTNSStringFromStringNilIfEmpty(item.sfSymbol) activeTintColor:RCTUIColorFromSharedColor(item.activeTintColor) - hidden:item.hidden]; + hidden:item.hidden + testID:RCTNSStringFromStringNilIfEmpty(item.testID)]; [result addObject:tabInfo]; } diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index 636cf72c..5551364d 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -96,6 +96,7 @@ struct TabViewImpl: View { sfSymbol: tabData?.sfSymbol, labeled: props.labeled ) + .accessibilityIdentifier(tabData?.testID ?? "") } .tag(tabData?.key) .tabBadge(tabData?.badge) diff --git a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift index daec61a5..3b634523 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift @@ -12,6 +12,7 @@ public final class TabInfo: NSObject { public let sfSymbol: String public let activeTintColor: PlatformColor? public let hidden: Bool + public let testID: String? public init( key: String, @@ -19,7 +20,8 @@ public final class TabInfo: NSObject { badge: String, sfSymbol: String, activeTintColor: PlatformColor?, - hidden: Bool + hidden: Bool, + testID: String? ) { self.key = key self.title = title @@ -27,6 +29,7 @@ public final class TabInfo: NSObject { self.sfSymbol = sfSymbol self.activeTintColor = activeTintColor self.hidden = hidden + self.testID = testID super.init() } } @@ -264,7 +267,8 @@ public final class TabInfo: NSObject { badge: itemDict["badge"] as? String ?? "", sfSymbol: itemDict["sfSymbol"] as? String ?? "", activeTintColor: RCTConvert.uiColor(itemDict["activeTintColor"] as? NSNumber), - hidden: itemDict["hidden"] as? Bool ?? false + hidden: itemDict["hidden"] as? Bool ?? false, + testID: itemDict["testID"] as? String ?? "" ) ) } diff --git a/packages/react-native-bottom-tabs/src/TabView.tsx b/packages/react-native-bottom-tabs/src/TabView.tsx index 5a24442d..7f56420f 100644 --- a/packages/react-native-bottom-tabs/src/TabView.tsx +++ b/packages/react-native-bottom-tabs/src/TabView.tsx @@ -111,6 +111,11 @@ interface Props { */ getHidden?: (props: { route: Route }) => boolean | undefined; + /** + * Get testID for the tab, uses `route.testID` by default. + */ + getTestID?: (props: { route: Route }) => string | undefined; + /** * Background color of the tab bar. */ @@ -164,6 +169,7 @@ const TabView = ({ barTintColor, getHidden = ({ route }: { route: Route }) => route.hidden, getActiveTintColor = ({ route }: { route: Route }) => route.activeTintColor, + getTestID = ({ route }: { route: Route }) => route.testID, hapticFeedbackEnabled = false, tabLabelStyle, ...props @@ -228,6 +234,7 @@ const TabView = ({ badge: getBadge?.({ route }), activeTintColor: processColor(getActiveTintColor({ route })), hidden: getHidden?.({ route }), + testID: getTestID?.({ route }), }; }), [ @@ -237,6 +244,7 @@ const TabView = ({ getBadge, getActiveTintColor, getHidden, + getTestID, ] ); diff --git a/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts b/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts index 2824e72c..981ed20b 100644 --- a/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts +++ b/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts @@ -29,6 +29,7 @@ export type TabViewItems = ReadonlyArray<{ badge?: string; activeTintColor?: ProcessedColorValue | null; hidden?: boolean; + testID?: string; }>; export interface TabViewProps extends ViewProps { diff --git a/packages/react-native-bottom-tabs/src/types.ts b/packages/react-native-bottom-tabs/src/types.ts index 8307f910..5993a61c 100644 --- a/packages/react-native-bottom-tabs/src/types.ts +++ b/packages/react-native-bottom-tabs/src/types.ts @@ -14,6 +14,7 @@ export type BaseRoute = { unfocusedIcon?: ImageSourcePropType | AppleIcon; activeTintColor?: string; hidden?: boolean; + testID?: string; }; export type NavigationState = { diff --git a/packages/react-navigation/src/types.ts b/packages/react-navigation/src/types.ts index e4c10708..eb1c3eb5 100644 --- a/packages/react-navigation/src/types.ts +++ b/packages/react-navigation/src/types.ts @@ -86,6 +86,11 @@ export type NativeBottomTabNavigationOptions = { * Active tab color. */ tabBarActiveTintColor?: string; + + /** + * TestID for the tab. + */ + tabBarButtonTestID?: string; }; export type NativeBottomTabDescriptor = Descriptor< @@ -111,5 +116,6 @@ export type NativeBottomTabNavigationConfig = Partial< | 'getBadge' | 'onTabLongPress' | 'getActiveTintColor' + | 'getTestID' > >; diff --git a/packages/react-navigation/src/views/NativeBottomTabView.tsx b/packages/react-navigation/src/views/NativeBottomTabView.tsx index b81a81b7..8c633f40 100644 --- a/packages/react-navigation/src/views/NativeBottomTabView.tsx +++ b/packages/react-navigation/src/views/NativeBottomTabView.tsx @@ -45,6 +45,9 @@ export default function NativeBottomTabView({ const options = descriptors[route.key]?.options; return options?.tabBarItemHidden === true; }} + getTestID={({ route }) => + descriptors[route.key]?.options.tabBarButtonTestID + } getIcon={({ route, focused }) => { const options = descriptors[route.key]?.options;