Skip to content

Commit 6f65cc3

Browse files
authored
refactor(tabs): replace isNativeAction with actionOrigin enum (#3949)
## Description Replaces the boolean `isNativeAction` field on the `onTabSelected` event with a three-valued `actionOrigin` discriminator (`'user' | 'programmatic-js' | 'implicit'`). Important thing to note here is that this change also enables us to add more action origins in the future, such as native-programmatic actions. One thing I'm not proud of is that the React model details are leaked into the `TabsContainer` (`TabsContainer` / `RNSTabBarController` on Android & iOS respectively). It now recognizes different sources including the `programmatic-js`. This is not great, but it shouldn't hinder us later when attempting to test it natively. The boolean was lossy: it conflated JS-driven updates with implicit UIKit-driven selection changes (e.g. iPad horizontal size-class transitions where UIKit reshuffles the selected tab as a side effect). Consumers that needed to react to JS-originated transitions specifically had no way to distinguish them. Promoting the field to an enum makes the actor explicit at the API boundary; on iOS it also lets us collapse the previous internal `RNSTabsNavigationStateUpdateSource` taxonomy into a single shared enum (`RNSTabsActionOrigin`). ## Changes - Public TS API: `isNativeAction: boolean` → `actionOrigin: 'user' | 'programmatic-js' | 'implicit'` on `TabSelectedEvent` (and the matching codegen specs for iOS / Android native components). - iOS: - New `RNSTabsActionOrigin` enum (`User`, `ProgrammaticJs`, `Implicit`) declared in `RNSTabsNavigationState.h`. - Removed the now-redundant `RNSTabsNavigationStateUpdateSource` enum; `progressNavigationState:withSource:` becomes `progressNavigationState:withOrigin:`. - `RNSTabsNavigationStateUpdateContext.isNativeAction` → `actionOrigin`; propagated through `OnTabSelectedPayload` and the event emitter, with an Obj-C → codegen-enum conversion in `RNSConversions-Tabs.mm`. - The implicit reconciliation path (More-controller size-class transition) now reports `Implicit` instead of being lumped into `isNativeAction = YES`. - Android: - New `TabsActionOrigin` enum (`USER`, `PROGRAMMATIC_JS`) under `gamma.tabs.container`. `'implicit'` is iOS-only at the moment and not currently produced on Android — documented in the enum's KDoc. - Threaded through `TabsContainerDelegate.onNavStateUpdate`, `TabsHostEventEmitter`, and `TabsHostTabSelectedEvent` (event payload key `actionOrigin`, serialized via `enum.toString()`). ## Test plan No new automated tests; this is a rename/refactor of an existing public field with no behavioral change on the user/JS paths. The implicit path on iOS now reports a distinct origin where it previously reported `isNativeAction = YES`, so manual verification on the affected platforms: - iOS — tab bar tap → `actionOrigin === 'user'`. - iOS — JS-driven `navStateRequest` update → `actionOrigin === 'programmatic-js'`. - iOS — iPad horizontal size-class transition that collapses/expands the More controller → `actionOrigin === 'implicit'` (previously `isNativeAction: true`, now distinguishable). - Android — tab bar tap → `actionOrigin === 'user'`; JS-driven update → `actionOrigin === 'programmatic-js'`. Reproduction lives in `FabricExample` Tabs test screens; log `e.nativeEvent.actionOrigin` from `onTabSelected`. ## Checklist - [ ] Included code example that can be used to test this change. - [ ] For visual changes, included screenshots / GIFs / recordings documenting the change. - [x] For API changes, updated relevant public types. - [ ] Ensured that CI passes
1 parent ec0ea94 commit 6f65cc3

17 files changed

Lines changed: 101 additions & 46 deletions
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.swmansion.rnscreens.gamma.tabs.container
2+
3+
/**
4+
* Origin (actor) that requested a tab transition. Mirrors the public `actionOrigin` event field.
5+
*
6+
* - [USER] — direct native UI interaction (tab bar tap).
7+
* - [PROGRAMMATIC_JS] — JS-initiated request delivered via the `navStateRequest` prop.
8+
*
9+
* The `implicit` origin defined on the public TS API is iOS-only at the moment;
10+
* Android does not currently produce it.
11+
*/
12+
enum class TabsActionOrigin {
13+
USER,
14+
PROGRAMMATIC_JS,
15+
;
16+
17+
override fun toString(): String =
18+
when (this) {
19+
USER -> "user"
20+
PROGRAMMATIC_JS -> "programmatic-js"
21+
}
22+
}

android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsContainer.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ internal class TabsContainer(
377377
navState,
378378
isRepeated = isRepeated,
379379
hasTriggeredSpecialEffect = hasTriggeredSpecialEffect,
380-
isNativeAction = !isInExternalOperationContext,
380+
actionOrigin = if (isInExternalOperationContext) TabsActionOrigin.PROGRAMMATIC_JS else TabsActionOrigin.USER,
381381
)
382382
}
383383

android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsContainerDelegate.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ internal interface TabsContainerDelegate {
1111
* @param navState The new navigation state after the change.
1212
* @param isRepeated Whether the same tab that was already selected has been selected again.
1313
* @param hasTriggeredSpecialEffect Whether a special effect (e.g. scroll-to-top) was triggered.
14-
* @param isNativeAction Whether the change was initiated by a native user action (tap).
14+
* @param actionOrigin Origin (actor) that requested this transition.
1515
*/
1616
fun onNavStateUpdate(
1717
navState: TabsNavState,
1818
isRepeated: Boolean,
1919
hasTriggeredSpecialEffect: Boolean,
20-
isNativeAction: Boolean,
20+
actionOrigin: TabsActionOrigin,
2121
)
2222

2323
/**

android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHost.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.facebook.react.uimanager.UIManagerHelper
1313
import com.swmansion.rnscreens.gamma.common.colorscheme.ColorScheme
1414
import com.swmansion.rnscreens.gamma.helpers.getFabricUIManagerNotNull
1515
import com.swmansion.rnscreens.gamma.tabs.container.TabSelectOp
16+
import com.swmansion.rnscreens.gamma.tabs.container.TabsActionOrigin
1617
import com.swmansion.rnscreens.gamma.tabs.container.TabsContainer
1718
import com.swmansion.rnscreens.gamma.tabs.container.TabsContainerDelegate
1819
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavState
@@ -159,14 +160,14 @@ class TabsHost(
159160
navState: TabsNavState,
160161
isRepeated: Boolean,
161162
hasTriggeredSpecialEffect: Boolean,
162-
isNativeAction: Boolean,
163+
actionOrigin: TabsActionOrigin,
163164
) {
164165
eventEmitter.emitOnTabSelectedEvent(
165166
navState.selectedKey,
166167
navState.provenance,
167168
isRepeated,
168169
hasTriggeredSpecialEffect,
169-
isNativeAction,
170+
actionOrigin,
170171
)
171172
}
172173

android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHostEventEmitter.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.swmansion.rnscreens.gamma.tabs.host
22

33
import com.facebook.react.bridge.ReactContext
44
import com.swmansion.rnscreens.gamma.common.event.BaseEventEmitter
5+
import com.swmansion.rnscreens.gamma.tabs.container.TabsActionOrigin
56
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavState
67
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavStateUpdateRejectionReason
78
import com.swmansion.rnscreens.gamma.tabs.host.event.TabsHostTabSelectedEvent
@@ -20,7 +21,7 @@ internal class TabsHostEventEmitter(
2021
provenance: Int,
2122
isRepeated: Boolean,
2223
hasTriggeredSpecialEffect: Boolean,
23-
isNativeAction: Boolean,
24+
actionOrigin: TabsActionOrigin,
2425
) {
2526
reactEventDispatcher.dispatchEvent(
2627
TabsHostTabSelectedEvent(
@@ -30,7 +31,7 @@ internal class TabsHostEventEmitter(
3031
provenance,
3132
isRepeated,
3233
hasTriggeredSpecialEffect,
33-
isNativeAction,
34+
actionOrigin,
3435
),
3536
)
3637
}

android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/event/TabsHostTabSelectedEvent.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.facebook.react.bridge.Arguments
44
import com.facebook.react.bridge.WritableMap
55
import com.facebook.react.uimanager.events.Event
66
import com.swmansion.rnscreens.gamma.common.event.NamingAwareEventType
7+
import com.swmansion.rnscreens.gamma.tabs.container.TabsActionOrigin
78

89
class TabsHostTabSelectedEvent(
910
surfaceId: Int,
@@ -12,7 +13,7 @@ class TabsHostTabSelectedEvent(
1213
val provenance: Int,
1314
val isRepeated: Boolean,
1415
val hasTriggeredSpecialEffect: Boolean,
15-
val isNativeAction: Boolean,
16+
val actionOrigin: TabsActionOrigin,
1617
) : Event<TabsHostTabSelectedEvent>(surfaceId, viewId),
1718
NamingAwareEventType {
1819
override fun getEventName() = EVENT_NAME
@@ -28,7 +29,7 @@ class TabsHostTabSelectedEvent(
2829
putInt(EK_PROVENANCE, provenance)
2930
putBoolean(EK_IS_REPEATED, isRepeated)
3031
putBoolean(EK_HAS_TRIGGERED_SPECIAL_EFFECT, hasTriggeredSpecialEffect)
31-
putBoolean(EK_IS_NATIVE_ACTION, isNativeAction)
32+
putString(EK_ACTION_ORIGIN, actionOrigin.toString())
3233
}
3334

3435
companion object : NamingAwareEventType {
@@ -39,7 +40,7 @@ class TabsHostTabSelectedEvent(
3940
private const val EK_PROVENANCE = "provenance"
4041
private const val EK_IS_REPEATED = "isRepeated"
4142
private const val EK_HAS_TRIGGERED_SPECIAL_EFFECT = "hasTriggeredSpecialEffect"
42-
private const val EK_IS_NATIVE_ACTION = "isNativeAction"
43+
private const val EK_ACTION_ORIGIN = "actionOrigin"
4344

4445
override fun getEventName() = EVENT_NAME
4546

ios/conversion/RNSConversions-Tabs.mm

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,23 @@ UITabBarControllerMode UITabBarControllerModeFromRNSTabBarControllerMode(RNSTabB
233233
}
234234
}
235235

236+
react::RNSTabsHostIOSEventEmitter::OnTabSelectedActionOrigin RNSOnTabSelectedActionOriginFromRNSTabsActionOrigin(
237+
RNSTabsActionOrigin actionOrigin)
238+
{
239+
using enum facebook::react::RNSTabsHostIOSEventEmitter::OnTabSelectedActionOrigin;
240+
switch (actionOrigin) {
241+
case RNSTabsActionOriginUser:
242+
return User;
243+
case RNSTabsActionOriginProgrammaticJs:
244+
return ProgrammaticJs;
245+
case RNSTabsActionOriginImplicit:
246+
return Implicit;
247+
default:
248+
RCTLogError(@"[RNScreens] Unexpected actionOrigin: %ld", actionOrigin);
249+
}
250+
return User;
251+
}
252+
236253
RNSTabsIconType RNSTabsIconTypeFromIcon(react::RNSTabsScreenIOSIconType iconType)
237254
{
238255
using enum facebook::react::RNSTabsScreenIOSIconType;

ios/conversion/RNSConversions.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ react::RNSTabsHostIOSEventEmitter::OnTabSelectionRejectedRejectionReason
6363
RNSOnTabSelectionRejectedRejectionReasonFromRNSTabsNavigationStateRejectionReason(
6464
RNSTabsNavigationStateRejectionReason reason);
6565

66+
react::RNSTabsHostIOSEventEmitter::OnTabSelectedActionOrigin RNSOnTabSelectedActionOriginFromRNSTabsActionOrigin(
67+
RNSTabsActionOrigin actionOrigin);
68+
6669
RNSTabsIconType RNSTabsIconTypeFromIcon(react::RNSTabsScreenIOSIconType iconType);
6770

6871
RNSTabsScreenSystemItem RNSTabsScreenSystemItemFromReactRNSTabsScreenSystemItem(

ios/tabs/host/RNSTabBarController.mm

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ - (BOOL)updateSelectedViewControllerTo:(nullable UIViewController *)nextSelected
236236
![NSString rnscreens_isBlankOrNull:screenKey],
237237
@"[RNScreens] The screenKey MUST NOT be null if the view controller is not null");
238238

239-
[self progressNavigationState:screenKey withSource:RNSTabsNavigationStateUpdateSourceExternal];
239+
[self progressNavigationState:screenKey withOrigin:RNSTabsActionOriginProgrammaticJs];
240240

241241
if (currSelectedViewController == nextSelectedViewController) {
242242
return YES;
@@ -254,8 +254,7 @@ - (BOOL)updateSelectedViewControllerTo:(nullable UIViewController *)nextSelected
254254
*/
255255
- (void)updateNavigationStateOnModelUpdate
256256
{
257-
[self progressNavigationState:[self screenKeyForSelectedViewController]
258-
withSource:RNSTabsNavigationStateUpdateSourceUser];
257+
[self progressNavigationState:[self screenKeyForSelectedViewController] withOrigin:RNSTabsActionOriginUser];
259258
}
260259

261260
- (void)userDidRepeatViewControllerSelection:(nonnull UIViewController *)viewController
@@ -277,7 +276,7 @@ - (void)userDidRepeatViewControllerSelection:(nonnull UIViewController *)viewCon
277276
[[RNSTabsNavigationStateUpdateContext alloc] initWithNavState:_navigationState
278277
isRepeated:YES
279278
hasTriggeredSpecialEffect:repeatedSelectionHandledBySpecialEffect
280-
isNativeAction:YES];
279+
actionOrigin:RNSTabsActionOriginUser];
281280
[self.tabsHostComponentView tabBarController:self didUpdateStateTo:_navigationState withContext:updateContext];
282281
}
283282

@@ -299,7 +298,7 @@ - (void)userDidSelectViewController:(nonnull UIViewController *)viewController
299298
auto *updateContext = [[RNSTabsNavigationStateUpdateContext alloc] initWithNavState:_navigationState
300299
isRepeated:NO
301300
hasTriggeredSpecialEffect:NO
302-
isNativeAction:YES];
301+
actionOrigin:RNSTabsActionOriginUser];
303302
[self.tabsHostComponentView tabBarController:self didUpdateStateTo:_navigationState withContext:updateContext];
304303
}
305304
}
@@ -517,7 +516,7 @@ - (void)updateSelectedViewControllerInner
517516
[[RNSTabsNavigationStateUpdateContext alloc] initWithNavState:_navigationState
518517
isRepeated:NO
519518
hasTriggeredSpecialEffect:NO
520-
isNativeAction:NO];
519+
actionOrigin:RNSTabsActionOriginProgrammaticJs];
521520
[self.tabsHostComponentView tabBarController:self didUpdateStateTo:_navigationState withContext:context];
522521
}
523522
}
@@ -574,8 +573,7 @@ - (nullable RNSTabsScreenViewController *)findChildViewControllerForKey:(nullabl
574573
return nil;
575574
}
576575

577-
- (void)progressNavigationState:(nonnull NSString *)newSelectedScreenKey
578-
withSource:(RNSTabsNavigationStateUpdateSource)updateSource
576+
- (void)progressNavigationState:(nonnull NSString *)newSelectedScreenKey withOrigin:(RNSTabsActionOrigin)origin
579577
{
580578
RCTAssert(newSelectedScreenKey != nil, @"[RNScreens] newSelectedScreenKey MUST NOT be nil");
581579

@@ -587,7 +585,7 @@ - (void)progressNavigationState:(nonnull NSString *)newSelectedScreenKey
587585
_navigationState = [RNSTabsNavigationState stateWithSelectedScreenKey:newSelectedScreenKey
588586
provenance:_navigationState.provenance + 1];
589587

590-
if (updateSource != RNSTabsNavigationStateUpdateSourceExternal) {
588+
if (origin != RNSTabsActionOriginProgrammaticJs) {
591589
_lastUINavigationState = [_navigationState cloneState];
592590
}
593591
}
@@ -666,12 +664,12 @@ - (void)reconcileNavigationStateWithUIKitState
666664
@"TabBarCtrl reconcileNavigationStateWithUIKitState: %@ -> %@",
667665
_navigationState.selectedScreenKey,
668666
selectedScreenKey);
669-
[self progressNavigationState:selectedScreenKey withSource:RNSTabsNavigationStateUpdateSourceImplicit];
667+
[self progressNavigationState:selectedScreenKey withOrigin:RNSTabsActionOriginImplicit];
670668

671669
auto *context = [[RNSTabsNavigationStateUpdateContext alloc] initWithNavState:_navigationState
672670
isRepeated:NO
673671
hasTriggeredSpecialEffect:NO
674-
isNativeAction:YES];
672+
actionOrigin:RNSTabsActionOriginImplicit];
675673
[self.tabsHostComponentView tabBarController:self didUpdateStateTo:_navigationState withContext:context];
676674
}
677675

ios/tabs/host/RNSTabsHostComponentView.mm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,7 @@ - (void)tabBarController:(nonnull RNSTabBarController *)tabBarController
609609
.provenance = navState.provenance,
610610
.isRepeated = context.isRepeated,
611611
.hasTriggeredSpecialEffect = context.hasTriggeredSpecialEffect,
612-
.isNativeAction = context.isNativeAction}];
612+
.actionOrigin = context.actionOrigin}];
613613
}
614614

615615
- (void)tabBarController:(nonnull RNSTabBarController *)tabBarController

0 commit comments

Comments
 (0)