Skip to content

Commit 993b1aa

Browse files
feat: add tab roles (#390)
* fix: move LegacyTabView measureView * fix: revert move LegacyTabView measureView * fix: move LegacyTabView measureView * fix: revert move LegacyTabView measureView * feat: add role prop * feat: add role handling * feat: add ts props * chore: update example * fix(android): assign role in RCTTabViewImpl * chore: run swiftlint * fix: export TabRole type * chore: add changelog entry * fix(android): remove role prop * fix: Update role jsdoc * feat: implement enum TabBarRole * feat: update TabInfo role type * feat: use TabBarRole.convert() * Update Podfile.lock fix: sync Podfile.lock * fix: rename tabBarRole to role * feat: update react-navigation docs * feat: update standalone docs * fix: adjust fixes from codereview --------- Co-authored-by: PADO <[email protected]>
1 parent e25a21a commit 993b1aa

File tree

15 files changed

+117
-58
lines changed

15 files changed

+117
-58
lines changed

.changeset/public-foxes-cheer.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'react-native-bottom-tabs': minor
3+
'@bottom-tabs/react-navigation': minor
4+
---
5+
6+
feat: add ios tab roles

apps/example/ios/Podfile.lock

Lines changed: 50 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1902,71 +1902,71 @@ SPEC CHECKSUMS:
19021902
FBLazyVector: 79c4b7ec726447eec5f8593379466bd9fde1aa14
19031903
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
19041904
glog: eb93e2f488219332457c3c4eafd2738ddc7e80b8
1905-
RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809
1905+
RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82
19061906
RCTDeprecation: 664055db806cce35c3c1b43c84414dd66e117ae6
19071907
RCTRequired: dc9a83fa1012054f94430d210337ca3a1afe6fc0
19081908
RCTTypeSafety: 031cefa254a1df313a196f105b8fcffdab1c5ab6
19091909
React: 8edfc46c315852ec88ea4a29d5e79019af3dc667
19101910
React-callinvoker: 4450b01574dfc7a8f074f7e29e6965ac04859c8f
1911-
React-Core: a318cda2bd04acffe4f70703098625b201aa2237
1912-
React-CoreModules: ebe93fa403bbd4d0909de105ffd34eeaad355083
1913-
React-cxxreact: 6fe3b8f8e8baf6a22fc39c8c4b4a0e1b5ae3e374
1911+
React-Core: 1fcd0d52ae09bdf7cf1fe96dd94b082208e43b86
1912+
React-CoreModules: 78e04d2319b1b61e0d4ed7fcd3e366d461819279
1913+
React-cxxreact: f9ca69323c1a9c22756ad1a4ed629fb6b44b2a18
19141914
React-debug: b0f7271aeacc2eb9e34f863397dcfc204ef721c0
1915-
React-defaultsnativemodule: 07704c5e30a5e66065a46aa11d4014941917a8ee
1916-
React-domnativemodule: dc3492f56861c82e658ea7db60316f7f1a8bace7
1917-
React-Fabric: 7a3d2abb0607f100881cc6c8e54484324c109260
1918-
React-FabricComponents: 4206a041e4671277d45deaf89e52f20951a8dd7d
1919-
React-FabricImage: b05580c4f17de740c38dd5e54d289e913f5bc5df
1915+
React-defaultsnativemodule: e790bf1d1a300a23504257f306a6629d2a60d845
1916+
React-domnativemodule: adcfebf6c3b7882b28a061ed2789777f3d337a18
1917+
React-Fabric: ecf387c8cca0e7ed2ef5104cd2725f38409163b2
1918+
React-FabricComponents: f7cd4fab1308f52c418b474dc877b94f7acc5672
1919+
React-FabricImage: e5c94c5679f2fec2261e76809aa86765fb4e0322
19201920
React-featureflags: 23d3dcdac6c9badeeb631db8a0883c7a3108d580
1921-
React-featureflagsnativemodule: 75c84559ad5fe3f26ccccdd34ee0320bbf191e4b
1922-
React-graphics: 61380f6d01a225af9a3808dfd0f16622d2b6f90d
1923-
React-idlecallbacksnativemodule: 58ef763402c13067c0e85c615caf7b389d661435
1924-
React-ImageManager: bab699b4ed44ce23b23d5bcab1cdc376eb69d583
1921+
React-featureflagsnativemodule: 7fc7346e83f792b6cbc851be03cbf201601a81fc
1922+
React-graphics: 348400b8ba57611d552af6db5dc7d42ccf132d08
1923+
React-idlecallbacksnativemodule: 8a111e8e0be17ef628ea58ce1c1b1587e331fc51
1924+
React-ImageManager: ee8526b1af93152133709104c6d649d5dada63b3
19251925
React-jsc: 1f6b8e576f2858c5479683647c081de145ce8055
1926-
React-jserrorhandler: f46bec9688c2fd853d101e7fb39ee48e162d5077
1927-
React-jsi: 917f26392eaec18d7ce4e197eb87f680ca87e426
1928-
React-jsiexecutor: 73a715d55e8ee66377ffe831063c5b7bb1a81448
1929-
React-jsinspector: e4ba333f8ed2bf14f0f5459482a28ea922145169
1930-
React-jsitracing: 838bbd073e24e84cf936354f085721cbc9204d70
1931-
React-logger: 1935d6e6461e9c8be4c87af56c56a4876021171e
1932-
React-Mapbuffer: 212171f037e3b22e6c2df839aa826806da480b85
1933-
React-microtasksnativemodule: a0ffd165c39251512d8cf51e9e8f5719dabc38b6
1934-
react-native-bottom-tabs: 6ee03990297f7e37f5c1dd6f5259cee851733d4f
1935-
react-native-safe-area-context: e54b360402f089600c2fb0d825d1d3d918b99e15
1926+
React-jserrorhandler: 51806588d8259e44cab7f35e72468be6ab8f6798
1927+
React-jsi: a75033c737fbcb46d80c80fc20b9475bfbf8d2bf
1928+
React-jsiexecutor: ed2125b6786f75b40cc5e3da791d7ab78a13e711
1929+
React-jsinspector: 22c5bd5e056328a95678e8f8df9e757171dd21fd
1930+
React-jsitracing: 9e7066f99151f99ed588f2055e011845b12a1bf6
1931+
React-logger: e7eeebaed32b88dcc29b10901aa8c5822dc397c4
1932+
React-Mapbuffer: 73dd1210c4ecf0dfb4e2d4e06f2a13f824a801a9
1933+
React-microtasksnativemodule: dece61f766f8d326099d217603b1ebb50d6bb707
1934+
react-native-bottom-tabs: 9cfaa0350a9632efff1e7ce16e732e32f06c6117
1935+
react-native-safe-area-context: 7f3dfe7a0e269ffccac6f1a8377e85e8237b47be
19361936
React-nativeconfig: cb207ebba7cafce30657c7ad9f1587a8f32e4564
1937-
React-NativeModulesApple: 76a5d35322908fbc88871e6dd20433bea2b8b2db
1938-
React-perflogger: 8152bab3f0eb4b8751f282f9af7caed2c823a9ea
1939-
React-performancetimeline: 3ef4a640b56f9c7ec5f52bd93217b9b607c37cf4
1937+
React-NativeModulesApple: 38f252170af5351c88bc2e94d697359cd8c031e6
1938+
React-perflogger: c4c3b7c18f8a50cdbe2bcdd2f15705ba029a5a02
1939+
React-performancetimeline: 38bda258bd9f9da19b27615e8edfbec064aa42cc
19401940
React-RCTActionSheet: 0fdf55fb8724856d63ca8c63cdb4e2325e15e8ec
1941-
React-RCTAnimation: b93f5a1675cc2599e96851fec13c909fdfb1d6bb
1942-
React-RCTAppDelegate: e3127aff7db7100ee0000e3f67956e9c6cbaa13f
1943-
React-RCTBlob: 53dc2afa8ccdc1b6d6885d81f6862fcb918a1875
1944-
React-RCTFabric: 0a4c2a18d0ef3368f900dc08ea15ab532dd3dcf3
1945-
React-RCTFBReactNativeSpec: ec50e74af2993fb51c1f9991cc7226fea21aaa26
1946-
React-RCTImage: 028171a4d7017ea96a2e605c817cd76f01ed3836
1947-
React-RCTLinking: e3f5431ab5f8f56b82387d41a2c484a278a8e645
1948-
React-RCTNetwork: 6de20da228ffe8bd9c9e3bafe3f7d1dfe1d7bd55
1949-
React-RCTSettings: 433c9f6a070bcecbe5a44d5009326b4d6f3b0667
1950-
React-RCTText: 46249950f8d8738b90a60883d19b5bef09f0a296
1951-
React-RCTVibration: 8f41e85ab6d40c7db6111ca9e8c7492c8de374fb
1941+
React-RCTAnimation: b2fcc7c462f1fb5e195a5547f6e405ec9a60d80f
1942+
React-RCTAppDelegate: a569d1037a16f4911bcf4d0b874598c7722fe2d5
1943+
React-RCTBlob: f6620374c96915ce1762405b1504e607e239c518
1944+
React-RCTFabric: d74ed998450607ebaf701e38969c04d85a54a1b2
1945+
React-RCTFBReactNativeSpec: cf67e0c357ed8de018a1ed5b33a8b258de651f5e
1946+
React-RCTImage: f189ae651e3c97879b4cdefcba1d4cffe55439da
1947+
React-RCTLinking: 759ac5e4aed95ac3c29849f98ff3f3b5ece830ed
1948+
React-RCTNetwork: ce1f38434a70eb1e228344f7632e636c3ceca03b
1949+
React-RCTSettings: 3602ea3adf9009f6d09461bf05f7e392414c32d8
1950+
React-RCTText: e48b4b54eab3f4cfea9be1228b5ef9ad3b8172c1
1951+
React-RCTVibration: 2e4dc335dd1e57c7004bcc07e7f5319e5968d5cf
19521952
React-rendererconsistency: c766ce7261ab6ed6be7bc155c403e29436d4f156
1953-
React-rendererdebug: 1f619b295f346242842f3accee23e8394b995d3c
1953+
React-rendererdebug: f8bf864b2646944c3f7c41555dbed0b5d7aea5d1
19541954
React-rncore: cafe45e14d870bbecbbf4bd89e12ef3b596e1f2d
1955-
React-RuntimeApple: 51303fe6715be3596bf0479c1b34ce56d61ac81f
1956-
React-RuntimeCore: a0fe52c5f42a65f9d636ee4fbee14322865eb530
1955+
React-RuntimeApple: 6b67a8f0109a5289ccef380d14ba099aeadcef0e
1956+
React-RuntimeCore: 056d99b829e1de4afed419e17e95639cf72799f1
19571957
React-runtimeexecutor: 201311bdafb53b5c30292782c8ee90193af86d91
1958-
React-runtimescheduler: 89a12fb995740bf1c1d768d3c6732e709913dbeb
1958+
React-runtimescheduler: 32e558eb10b88ea398bb974b74d2230e5a71f30e
19591959
React-timing: 127d8598b5a15ae5b29ebd0ec474d590285c6f2f
1960-
React-utils: 5157cba7e171651af2113558b0c6cd562d4271bf
1961-
ReactAppDependencyProvider: e7e92253013754a8c35ebdbf8ad700f4e8956f62
1962-
ReactCodegen: 6efd314e2f59c2eae0898c6d1e0d933876a1666c
1963-
ReactCommon: cec0154a884747940be235f16acbd4fc9c959f89
1964-
ReactNativeHost: 9796a3872d3b2777a87acbe62d666dec521eda7b
1965-
ReactTestApp-DevSupport: ba03e8b8d2c87ed4b631ae8dc25765925f37e4a7
1960+
React-utils: d5269d138fd5b7b93a7f03e697f25d482e64d399
1961+
ReactAppDependencyProvider: 41e9fb63606c32cce924653d2d410cb01ec81286
1962+
ReactCodegen: 9cf993a8cfdffca67d5abe1ba056020fc48fc0d4
1963+
ReactCommon: ede76856e587ac3fd7ce70ca2387e571bc947d14
1964+
ReactNativeHost: e48e75303e422f7966cf7dc4b68d4b59d4570217
1965+
ReactTestApp-DevSupport: f23bacc6d21da29a7d8d248bb6ee8cc9ad241a48
19661966
ReactTestApp-Resources: 4f6dff3b157f879757cd750caccd1d34a7eda647
1967-
RNGestureHandler: 3bd32689c176c81dd57bb1e3b3804e7352994000
1968-
RNScreens: be44c347c9ae035bc78da28e283f484fa37e916f
1969-
RNVectorIcons: 5987b681d1ad97637f67e4e7af2902b9d4c3f5d6
1967+
RNGestureHandler: 5efafa1473ccab9c4530bb7310032b784ecc84a6
1968+
RNScreens: 749bdbb62d3dc57bd1373c64eaf5342ea0768178
1969+
RNVectorIcons: 3bcc7d69519bcac82308d2464579c4203a451c28
19701970
SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8
19711971
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
19721972
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748

apps/example/src/Examples/SFSymbols.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export default function SFSymbols() {
4141
? require('../../assets/icons/person_dark.png')
4242
: { sfSymbol: 'person.fill' },
4343
title: 'Contacts',
44+
role: 'search',
4445
},
4546
]);
4647

docs/docs/docs/guides/standalone-usage.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ Each route in the `routes` array can have the following properties:
212212
- `activeTintColor`: Custom active tint color for this specific tab
213213
- `lazy`: Whether to lazy load this tab's content
214214
- `freezeOnBlur`: Whether to freeze the tab's content when it's not visible
215+
- `role`: A value that defines the purpose of the tab
215216

216217
### Helper Props
217218

@@ -260,3 +261,9 @@ Function to determine if a tab should be hidden.
260261
Function to get the test ID for a tab item.
261262

262263
- Default: Uses `route.testID`
264+
265+
#### `getRole`
266+
267+
Function to get the role for a tab item.
268+
269+
- Default: Uses `route.role`

docs/docs/docs/guides/usage-with-react-navigation.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,14 @@ It's working separately from `enableFreeze()` in `react-native-screens`. So sett
326326

327327
Test ID for the tab item. This can be used to find the tab item in the native view hierarchy.
328328

329+
#### `role` <Badge text="iOS 18+" type="info" />
330+
331+
A value that defines the purpose of the tab. This can be used to pin and separate search tabs
332+
333+
Available options:
334+
335+
- `search` - The search role.
336+
329337
### Events
330338

331339
The navigator can emit events on certain actions. Supported events are:

packages/react-native-bottom-tabs/ios/Fabric/RCTTabViewComponentView.mm

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
lhs.badge == rhs.badge &&
3838
lhs.activeTintColor == rhs.activeTintColor &&
3939
lhs.hidden == rhs.hidden &&
40-
lhs.testID == rhs.testID;
40+
lhs.testID == rhs.testID &&
41+
lhs.role == rhs.role;
4142
}
4243

4344
bool operator!=(const RNCTabViewItemsStruct& lhs, const RNCTabViewItemsStruct& rhs) {
@@ -201,7 +202,8 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
201202
sfSymbol:RCTNSStringFromStringNilIfEmpty(item.sfSymbol)
202203
activeTintColor:RCTUIColorFromSharedColor(item.activeTintColor)
203204
hidden:item.hidden
204-
testID:RCTNSStringFromStringNilIfEmpty(item.testID)];
205+
testID:RCTNSStringFromStringNilIfEmpty(item.testID)
206+
role:RCTNSStringFromStringNilIfEmpty(item.role)];
205207

206208
[result addObject:tabInfo];
207209
}

packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ struct NewTabView: AnyTabView {
2828
onSelect: onSelect
2929
)
3030

31-
Tab(value: tabData.key) {
31+
Tab(value: tabData.key, role: tabData.role?.convert()) {
3232
child
3333
.ignoresSafeArea(.container, edges: .all)
3434
.tabAppear(using: context)

packages/react-native-bottom-tabs/ios/TabViewProps.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ internal enum MinimizeBehavior: String {
2323
#endif
2424
}
2525

26+
public enum TabBarRole: String {
27+
case search
28+
29+
@available(iOS 18, macOS 15, visionOS 2, tvOS 18, *)
30+
func convert() -> TabRole {
31+
switch self {
32+
case .search:
33+
return .search
34+
}
35+
}
36+
}
37+
2638
/**
2739
Props that component accepts. SwiftUI view gets re-rendered when ObservableObject changes.
2840
*/

packages/react-native-bottom-tabs/ios/TabViewProvider.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public final class TabInfo: NSObject {
1313
public let activeTintColor: PlatformColor?
1414
public let hidden: Bool
1515
public let testID: String?
16+
public let role: TabBarRole?
1617

1718
public init(
1819
key: String,
@@ -21,7 +22,8 @@ public final class TabInfo: NSObject {
2122
sfSymbol: String,
2223
activeTintColor: PlatformColor?,
2324
hidden: Bool,
24-
testID: String?
25+
testID: String?,
26+
role: String?
2527
) {
2628
self.key = key
2729
self.title = title
@@ -30,6 +32,7 @@ public final class TabInfo: NSObject {
3032
self.activeTintColor = activeTintColor
3133
self.hidden = hidden
3234
self.testID = testID
35+
self.role = TabBarRole(rawValue: role ?? "")
3336
super.init()
3437
}
3538
}
@@ -273,7 +276,8 @@ public final class TabInfo: NSObject {
273276
sfSymbol: itemDict["sfSymbol"] as? String ?? "",
274277
activeTintColor: RCTConvert.uiColor(itemDict["activeTintColor"] as? NSNumber),
275278
hidden: itemDict["hidden"] as? Bool ?? false,
276-
testID: itemDict["testID"] as? String ?? ""
279+
testID: itemDict["testID"] as? String ?? "",
280+
role: itemDict["role"] as? String
277281
)
278282
)
279283
}

packages/react-native-bottom-tabs/src/TabView.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { BottomTabBarHeightContext } from './utils/BottomTabBarHeightContext';
2020
import type { ImageSource } from 'react-native/Libraries/Image/ImageSource';
2121
import NativeTabView from './TabViewNativeComponent';
2222
import useLatestCallback from 'use-latest-callback';
23-
import type { BaseRoute, NavigationState } from './types';
23+
import type { BaseRoute, NavigationState, TabRole } from './types';
2424
import DelayedFreeze from './DelayedFreeze';
2525

2626
const isAppleSymbol = (icon: any): icon is { sfSymbol: string } =>
@@ -124,6 +124,11 @@ interface Props<Route extends BaseRoute> {
124124
*/
125125
getTestID?: (props: { route: Route }) => string | undefined;
126126

127+
/**
128+
* Get role for the tab, uses `route.role` by default. (iOS only)
129+
*/
130+
getRole?: (props: { route: Route }) => TabRole | undefined;
131+
127132
/**
128133
* Custom tab bar to render. Set to `null` to hide the tab bar completely.
129134
*/
@@ -190,6 +195,7 @@ const TabView = <Route extends BaseRoute>({
190195
getHidden = ({ route }: { route: Route }) => route.hidden,
191196
getActiveTintColor = ({ route }: { route: Route }) => route.activeTintColor,
192197
getTestID = ({ route }: { route: Route }) => route.testID,
198+
getRole = ({ route }: { route: Route }) => route.role,
193199
hapticFeedbackEnabled = false,
194200
// Android's native behavior is to show labels when there are less than 4 tabs. We leave it as undefined to use the platform default behavior.
195201
labeled = Platform.OS !== 'android' ? true : undefined,
@@ -261,6 +267,7 @@ const TabView = <Route extends BaseRoute>({
261267
activeTintColor: processColor(getActiveTintColor({ route })),
262268
hidden: getHidden?.({ route }),
263269
testID: getTestID?.({ route }),
270+
role: getRole?.({ route }),
264271
};
265272
}),
266273
[
@@ -271,6 +278,7 @@ const TabView = <Route extends BaseRoute>({
271278
getActiveTintColor,
272279
getHidden,
273280
getTestID,
281+
getRole,
274282
]
275283
);
276284

0 commit comments

Comments
 (0)