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
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.swmansion.rnscreens.gamma.tabs.container

/**
* Origin (actor) that requested a tab transition. Mirrors the public `actionOrigin` event field.
*
* - [USER] — direct native UI interaction (tab bar tap).
* - [PROGRAMMATIC_JS] — JS-initiated request delivered via the `navStateRequest` prop.
*
* The `implicit` origin defined on the public TS API is iOS-only at the moment;
* Android does not currently produce it.
*/
enum class TabsActionOrigin {
USER,
PROGRAMMATIC_JS,
;

override fun toString(): String =
when (this) {
USER -> "user"
PROGRAMMATIC_JS -> "programmatic-js"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ internal class TabsContainer(
navState,
isRepeated = isRepeated,
hasTriggeredSpecialEffect = hasTriggeredSpecialEffect,
isNativeAction = !isInExternalOperationContext,
actionOrigin = if (isInExternalOperationContext) TabsActionOrigin.PROGRAMMATIC_JS else TabsActionOrigin.USER,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ internal interface TabsContainerDelegate {
* @param navState The new navigation state after the change.
* @param isRepeated Whether the same tab that was already selected has been selected again.
* @param hasTriggeredSpecialEffect Whether a special effect (e.g. scroll-to-top) was triggered.
* @param isNativeAction Whether the change was initiated by a native user action (tap).
* @param actionOrigin Origin (actor) that requested this transition.
*/
fun onNavStateUpdate(
navState: TabsNavState,
isRepeated: Boolean,
hasTriggeredSpecialEffect: Boolean,
isNativeAction: Boolean,
actionOrigin: TabsActionOrigin,
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.facebook.react.uimanager.UIManagerHelper
import com.swmansion.rnscreens.gamma.common.colorscheme.ColorScheme
import com.swmansion.rnscreens.gamma.helpers.getFabricUIManagerNotNull
import com.swmansion.rnscreens.gamma.tabs.container.TabSelectOp
import com.swmansion.rnscreens.gamma.tabs.container.TabsActionOrigin
import com.swmansion.rnscreens.gamma.tabs.container.TabsContainer
import com.swmansion.rnscreens.gamma.tabs.container.TabsContainerDelegate
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavState
Expand Down Expand Up @@ -159,14 +160,14 @@ class TabsHost(
navState: TabsNavState,
isRepeated: Boolean,
hasTriggeredSpecialEffect: Boolean,
isNativeAction: Boolean,
actionOrigin: TabsActionOrigin,
) {
eventEmitter.emitOnTabSelectedEvent(
navState.selectedKey,
navState.provenance,
isRepeated,
hasTriggeredSpecialEffect,
isNativeAction,
actionOrigin,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.swmansion.rnscreens.gamma.tabs.host

import com.facebook.react.bridge.ReactContext
import com.swmansion.rnscreens.gamma.common.event.BaseEventEmitter
import com.swmansion.rnscreens.gamma.tabs.container.TabsActionOrigin
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavState
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavStateUpdateRejectionReason
import com.swmansion.rnscreens.gamma.tabs.host.event.TabsHostTabSelectedEvent
Expand All @@ -20,7 +21,7 @@ internal class TabsHostEventEmitter(
provenance: Int,
isRepeated: Boolean,
hasTriggeredSpecialEffect: Boolean,
isNativeAction: Boolean,
actionOrigin: TabsActionOrigin,
) {
reactEventDispatcher.dispatchEvent(
TabsHostTabSelectedEvent(
Expand All @@ -30,7 +31,7 @@ internal class TabsHostEventEmitter(
provenance,
isRepeated,
hasTriggeredSpecialEffect,
isNativeAction,
actionOrigin,
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
import com.swmansion.rnscreens.gamma.common.event.NamingAwareEventType
import com.swmansion.rnscreens.gamma.tabs.container.TabsActionOrigin

class TabsHostTabSelectedEvent(
surfaceId: Int,
Expand All @@ -12,7 +13,7 @@ class TabsHostTabSelectedEvent(
val provenance: Int,
val isRepeated: Boolean,
val hasTriggeredSpecialEffect: Boolean,
val isNativeAction: Boolean,
val actionOrigin: TabsActionOrigin,
) : Event<TabsHostTabSelectedEvent>(surfaceId, viewId),
NamingAwareEventType {
override fun getEventName() = EVENT_NAME
Expand All @@ -28,7 +29,7 @@ class TabsHostTabSelectedEvent(
putInt(EK_PROVENANCE, provenance)
putBoolean(EK_IS_REPEATED, isRepeated)
putBoolean(EK_HAS_TRIGGERED_SPECIAL_EFFECT, hasTriggeredSpecialEffect)
putBoolean(EK_IS_NATIVE_ACTION, isNativeAction)
putString(EK_ACTION_ORIGIN, actionOrigin.toString())
}

companion object : NamingAwareEventType {
Expand All @@ -39,7 +40,7 @@ class TabsHostTabSelectedEvent(
private const val EK_PROVENANCE = "provenance"
private const val EK_IS_REPEATED = "isRepeated"
private const val EK_HAS_TRIGGERED_SPECIAL_EFFECT = "hasTriggeredSpecialEffect"
private const val EK_IS_NATIVE_ACTION = "isNativeAction"
private const val EK_ACTION_ORIGIN = "actionOrigin"

override fun getEventName() = EVENT_NAME

Expand Down
17 changes: 17 additions & 0 deletions ios/conversion/RNSConversions-Tabs.mm
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,23 @@ UITabBarControllerMode UITabBarControllerModeFromRNSTabBarControllerMode(RNSTabB
}
}

react::RNSTabsHostIOSEventEmitter::OnTabSelectedActionOrigin RNSOnTabSelectedActionOriginFromRNSTabsActionOrigin(
RNSTabsActionOrigin actionOrigin)
{
using enum facebook::react::RNSTabsHostIOSEventEmitter::OnTabSelectedActionOrigin;
switch (actionOrigin) {
case RNSTabsActionOriginUser:
return User;
case RNSTabsActionOriginProgrammaticJs:
return ProgrammaticJs;
case RNSTabsActionOriginImplicit:
return Implicit;
default:
RCTLogError(@"[RNScreens] Unexpected actionOrigin: %ld", actionOrigin);
}
Comment thread
kkafar marked this conversation as resolved.
return User;
}

RNSTabsIconType RNSTabsIconTypeFromIcon(react::RNSTabsScreenIOSIconType iconType)
{
using enum facebook::react::RNSTabsScreenIOSIconType;
Expand Down
3 changes: 3 additions & 0 deletions ios/conversion/RNSConversions.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ react::RNSTabsHostIOSEventEmitter::OnTabSelectionRejectedRejectionReason
RNSOnTabSelectionRejectedRejectionReasonFromRNSTabsNavigationStateRejectionReason(
RNSTabsNavigationStateRejectionReason reason);

react::RNSTabsHostIOSEventEmitter::OnTabSelectedActionOrigin RNSOnTabSelectedActionOriginFromRNSTabsActionOrigin(
RNSTabsActionOrigin actionOrigin);

RNSTabsIconType RNSTabsIconTypeFromIcon(react::RNSTabsScreenIOSIconType iconType);

RNSTabsScreenSystemItem RNSTabsScreenSystemItemFromReactRNSTabsScreenSystemItem(
Expand Down
20 changes: 9 additions & 11 deletions ios/tabs/host/RNSTabBarController.mm
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ - (BOOL)updateSelectedViewControllerTo:(nullable UIViewController *)nextSelected
![NSString rnscreens_isBlankOrNull:screenKey],
@"[RNScreens] The screenKey MUST NOT be null if the view controller is not null");

[self progressNavigationState:screenKey withSource:RNSTabsNavigationStateUpdateSourceExternal];
[self progressNavigationState:screenKey withOrigin:RNSTabsActionOriginProgrammaticJs];
Comment thread
kligarski marked this conversation as resolved.

if (currSelectedViewController == nextSelectedViewController) {
return YES;
Expand All @@ -254,8 +254,7 @@ - (BOOL)updateSelectedViewControllerTo:(nullable UIViewController *)nextSelected
*/
- (void)updateNavigationStateOnModelUpdate
{
[self progressNavigationState:[self screenKeyForSelectedViewController]
withSource:RNSTabsNavigationStateUpdateSourceUser];
[self progressNavigationState:[self screenKeyForSelectedViewController] withOrigin:RNSTabsActionOriginUser];
}

- (void)userDidRepeatViewControllerSelection:(nonnull UIViewController *)viewController
Expand All @@ -277,7 +276,7 @@ - (void)userDidRepeatViewControllerSelection:(nonnull UIViewController *)viewCon
[[RNSTabsNavigationStateUpdateContext alloc] initWithNavState:_navigationState
isRepeated:YES
hasTriggeredSpecialEffect:repeatedSelectionHandledBySpecialEffect
isNativeAction:YES];
actionOrigin:RNSTabsActionOriginUser];
[self.tabsHostComponentView tabBarController:self didUpdateStateTo:_navigationState withContext:updateContext];
}

Expand All @@ -299,7 +298,7 @@ - (void)userDidSelectViewController:(nonnull UIViewController *)viewController
auto *updateContext = [[RNSTabsNavigationStateUpdateContext alloc] initWithNavState:_navigationState
isRepeated:NO
hasTriggeredSpecialEffect:NO
isNativeAction:YES];
actionOrigin:RNSTabsActionOriginUser];
[self.tabsHostComponentView tabBarController:self didUpdateStateTo:_navigationState withContext:updateContext];
}
}
Expand Down Expand Up @@ -517,7 +516,7 @@ - (void)updateSelectedViewControllerInner
[[RNSTabsNavigationStateUpdateContext alloc] initWithNavState:_navigationState
isRepeated:NO
hasTriggeredSpecialEffect:NO
isNativeAction:NO];
actionOrigin:RNSTabsActionOriginProgrammaticJs];
[self.tabsHostComponentView tabBarController:self didUpdateStateTo:_navigationState withContext:context];
}
}
Expand Down Expand Up @@ -574,8 +573,7 @@ - (nullable RNSTabsScreenViewController *)findChildViewControllerForKey:(nullabl
return nil;
}

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

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

if (updateSource != RNSTabsNavigationStateUpdateSourceExternal) {
if (origin != RNSTabsActionOriginProgrammaticJs) {
_lastUINavigationState = [_navigationState cloneState];
}
}
Expand Down Expand Up @@ -666,12 +664,12 @@ - (void)reconcileNavigationStateWithUIKitState
@"TabBarCtrl reconcileNavigationStateWithUIKitState: %@ -> %@",
_navigationState.selectedScreenKey,
selectedScreenKey);
[self progressNavigationState:selectedScreenKey withSource:RNSTabsNavigationStateUpdateSourceImplicit];
[self progressNavigationState:selectedScreenKey withOrigin:RNSTabsActionOriginImplicit];

auto *context = [[RNSTabsNavigationStateUpdateContext alloc] initWithNavState:_navigationState
isRepeated:NO
hasTriggeredSpecialEffect:NO
isNativeAction:YES];
actionOrigin:RNSTabsActionOriginImplicit];
[self.tabsHostComponentView tabBarController:self didUpdateStateTo:_navigationState withContext:context];
}

Expand Down
2 changes: 1 addition & 1 deletion ios/tabs/host/RNSTabsHostComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ - (void)tabBarController:(nonnull RNSTabBarController *)tabBarController
.provenance = navState.provenance,
.isRepeated = context.isRepeated,
.hasTriggeredSpecialEffect = context.hasTriggeredSpecialEffect,
.isNativeAction = context.isNativeAction}];
.actionOrigin = context.actionOrigin}];
}

- (void)tabBarController:(nonnull RNSTabBarController *)tabBarController
Expand Down
4 changes: 2 additions & 2 deletions ios/tabs/host/RNSTabsHostEventEmitter.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ typedef struct {
BOOL isRepeated;
/** Whether a special effect (e.g. scroll-to-top) was triggered. */
BOOL hasTriggeredSpecialEffect;
/** Whether the selection was initiated by a native user action (tap). */
BOOL isNativeAction;
/** Origin (actor) that requested this transition. */
RNSTabsActionOrigin actionOrigin;
} OnTabSelectedPayload;

/** Payload for the `onTabSelectionRejected` event emitted when a tab selection request is rejected. */
Expand Down
4 changes: 3 additions & 1 deletion ios/tabs/host/RNSTabsHostEventEmitter.mm
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ - (void)updateEventEmitter:(const std::shared_ptr<const react::RNSTabsHostIOSEve
- (BOOL)emitOnTabSelected:(OnTabSelectedPayload)payload
{
if (_reactEventEmitter != nullptr) {
auto convertedActionOrigin =
rnscreens::conversion::RNSOnTabSelectedActionOriginFromRNSTabsActionOrigin(payload.actionOrigin);
_reactEventEmitter->onTabSelected(
{.selectedScreenKey = RCTStringFromNSString(payload.selectedScreenKey),
.provenance = payload.provenance,
.isRepeated = static_cast<bool>(payload.isRepeated),
.hasTriggeredSpecialEffect = static_cast<bool>(payload.hasTriggeredSpecialEffect),
.isNativeAction = static_cast<bool>(payload.isNativeAction)});
.actionOrigin = convertedActionOrigin});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnTabSelected event emission due to nullish emitter");
Expand Down
32 changes: 18 additions & 14 deletions ios/tabs/host/RNSTabsNavigationState.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ NS_ASSUME_NONNULL_BEGIN

@end

/**
* Origin (actor) that requested a tab transition. Mirrors the public `actionOrigin` event field.
*
* - [User] direct native UI interaction (tab bar tap, drag-and-drop).
* - [ProgrammaticJs] JS-initiated request delivered via the `navStateRequest` prop.
* - [Implicit] platform side effect not attributable to an explicit actor — UIKit changed the selection
* as a side effect of another operation (e.g. More navigation controller disappearing during a
* horizontal size class transition on iPad).
*/
typedef NS_ENUM(NSInteger, RNSTabsActionOrigin) {
RNSTabsActionOriginUser = 0,
RNSTabsActionOriginProgrammaticJs,
RNSTabsActionOriginImplicit,
};

/** Bundles a navigation state change together with metadata about the selection context. */
@interface RNSTabsNavigationStateUpdateContext : NSObject

Expand All @@ -37,27 +52,16 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly) BOOL isRepeated;
/** Whether a special effect (e.g. scroll-to-top) was triggered by the selection. */
@property (nonatomic, readonly) BOOL hasTriggeredSpecialEffect;
/** Whether the selection was initiated by a native user action (tap) as opposed to a JS-driven update. */
@property (nonatomic, readonly) BOOL isNativeAction;
/** Origin (actor) that requested this transition. */
@property (nonatomic, readonly) RNSTabsActionOrigin actionOrigin;

- (instancetype)initWithNavState:(nonnull RNSTabsNavigationState *)navState
isRepeated:(BOOL)isRepeated
hasTriggeredSpecialEffect:(BOOL)hasTriggeredSpecialEffect
isNativeAction:(BOOL)isNativeAction;
actionOrigin:(RNSTabsActionOrigin)actionOrigin;

@end

/** Source of a navigation state update. */
typedef NS_ENUM(NSInteger, RNSTabsNavigationStateUpdateSource) {
/** Update initiated by a native user interaction (e.g. tab tap). */
RNSTabsNavigationStateUpdateSourceUser = 0,
/** Update initiated externally (e.g. from JS via props). */
RNSTabsNavigationStateUpdateSourceExternal,
/** Update detected implicitly — UIKit changed the selection as a side effect of another operation
* (e.g. More navigation controller disappearing during a horizontal size class transition on iPad). */
RNSTabsNavigationStateUpdateSourceImplicit
};

/** Reason why a navigation state update was rejected by the container. */
typedef NS_ENUM(NSInteger, RNSTabsNavigationStateRejectionReason) {
/** The update's provenance is based on a stale state. */
Expand Down
4 changes: 2 additions & 2 deletions ios/tabs/host/RNSTabsNavigationState.mm
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ @implementation RNSTabsNavigationStateUpdateContext
- (instancetype)initWithNavState:(nonnull RNSTabsNavigationState *)navState
isRepeated:(BOOL)isRepeated
hasTriggeredSpecialEffect:(BOOL)hasTriggeredSpecialEffect
isNativeAction:(BOOL)isNativeAction
actionOrigin:(RNSTabsActionOrigin)actionOrigin
{
if (self = [super init]) {
_navState = navState;
_isRepeated = isRepeated;
_hasTriggeredSpecialEffect = hasTriggeredSpecialEffect;
_isNativeAction = isNativeAction;
_actionOrigin = actionOrigin;
}
return self;
}
Expand Down
12 changes: 9 additions & 3 deletions src/components/tabs/host/TabsHost.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,16 @@ export type TabSelectedEvent = {
/** Whether the selection triggered a special effect (e.g. scroll-to-top on repeated selection). */
hasTriggeredSpecialEffect: boolean;
/**
* False in case the event is a result of JS-driven update. True otherwise, e.g. in case of user action (tap)
* or implicit UIKit action (app resize, orientation change, etc.).
* @summary Origin (actor) that requested this tab transition.
*
* @description
* - `user` — direct native UI interaction (e.g. tab bar tap, iOS tab drag-and-drop).
* - `programmatic-js` — JS-initiated request delivered via the `navStateRequest` prop.
* - `implicit` — platform side effect not attributable to an explicit actor
* (e.g. UIKit reshuffling the selection during a horizontal size-class transition on iPad).
* Currently only emitted on iOS.
*/
isNativeAction: boolean;
actionOrigin: 'user' | 'programmatic-js' | 'implicit';
};

/**
Expand Down
2 changes: 1 addition & 1 deletion src/fabric/tabs/TabsHostAndroidNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type TabSelectedEvent = {
provenance: CT.Int32;
isRepeated: boolean;
hasTriggeredSpecialEffect: boolean;
isNativeAction: boolean;
actionOrigin: 'user' | 'programmatic-js' | 'implicit';
};

type NavigationStateRequest = {
Expand Down
2 changes: 1 addition & 1 deletion src/fabric/tabs/TabsHostIOSNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type TabSelectedEvent = Readonly<{
provenance: CT.Int32;
isRepeated: boolean;
hasTriggeredSpecialEffect: boolean;
isNativeAction: boolean;
actionOrigin: 'user' | 'programmatic-js' | 'implicit';
}>;

type NavigationStateRequest = Readonly<{
Expand Down
Loading