react-native-screens 4.25.0-beta.1 → 4.25.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/StackHeaderAppBarLayout.kt +6 -14
  2. package/android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/StackHeaderAppBarLayoutBehavior.kt +29 -0
  3. package/android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/StackHeaderCoordinator.kt +56 -0
  4. package/android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/config/StackHeaderConfig.kt +11 -0
  5. package/android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/config/StackHeaderConfigProviding.kt +5 -0
  6. package/android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/config/StackHeaderConfigViewManager.kt +35 -0
  7. package/android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/subview/StackHeaderSubview.kt +3 -7
  8. package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsActionOrigin.kt +26 -0
  9. package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsContainer.kt +227 -151
  10. package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsNavigationState.kt +60 -0
  11. package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/{TabsContainerDelegate.kt → TabsNavigationStateObserver.kt} +19 -14
  12. package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsNavigationStateObserverRegistry.kt +88 -0
  13. package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHost.kt +40 -24
  14. package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHostEventEmitter.kt +11 -9
  15. package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHostViewManager.kt +19 -7
  16. package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/event/TabsHostTabSelectedEvent.kt +4 -3
  17. package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/event/TabsHostTabSelectionPreventedEvent.kt +3 -3
  18. package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/event/TabsHostTabSelectionRejectedEvent.kt +12 -11
  19. package/ios/conversion/RNSConversions-Tabs.mm +19 -0
  20. package/ios/conversion/RNSConversions.h +3 -0
  21. package/ios/tabs/bottom-accessory/RNSTabsBottomAccessoryHelper.mm +34 -5
  22. package/ios/tabs/host/RNSTabBarController.h +152 -99
  23. package/ios/tabs/host/RNSTabBarController.mm +137 -113
  24. package/ios/tabs/host/RNSTabsHostComponentView.h +7 -8
  25. package/ios/tabs/host/RNSTabsHostComponentView.mm +37 -33
  26. package/ios/tabs/host/RNSTabsHostEventEmitter.h +4 -4
  27. package/ios/tabs/host/RNSTabsHostEventEmitter.mm +5 -3
  28. package/ios/tabs/host/RNSTabsNavigationState.h +142 -27
  29. package/ios/tabs/host/RNSTabsNavigationState.mm +35 -2
  30. package/ios/tabs/host/RNSTabsNavigationStateObserverRegistry.h +62 -0
  31. package/ios/tabs/host/RNSTabsNavigationStateObserverRegistry.mm +104 -0
  32. package/lib/commonjs/components/gamma/stack/header/StackHeaderConfig.android.js +46 -1
  33. package/lib/commonjs/components/gamma/stack/header/StackHeaderConfig.android.js.map +1 -1
  34. package/lib/commonjs/components/safe-area/SafeAreaView.web.js +2 -3
  35. package/lib/commonjs/components/safe-area/SafeAreaView.web.js.map +1 -1
  36. package/lib/commonjs/components/tabs/host/TabsHost.android.js +2 -2
  37. package/lib/commonjs/components/tabs/host/TabsHost.android.js.map +1 -1
  38. package/lib/commonjs/components/tabs/host/TabsHost.ios.js +2 -2
  39. package/lib/commonjs/components/tabs/host/TabsHost.ios.js.map +1 -1
  40. package/lib/commonjs/fabric/gamma/stack/StackHeaderConfigAndroidNativeComponent.js.map +1 -1
  41. package/lib/commonjs/flags.js +1 -0
  42. package/lib/commonjs/flags.js.map +1 -1
  43. package/lib/module/components/gamma/stack/header/StackHeaderConfig.android.js +46 -1
  44. package/lib/module/components/gamma/stack/header/StackHeaderConfig.android.js.map +1 -1
  45. package/lib/module/components/safe-area/SafeAreaView.web.js +1 -1
  46. package/lib/module/components/safe-area/SafeAreaView.web.js.map +1 -1
  47. package/lib/module/components/tabs/host/TabsHost.android.js +2 -2
  48. package/lib/module/components/tabs/host/TabsHost.android.js.map +1 -1
  49. package/lib/module/components/tabs/host/TabsHost.ios.js +2 -2
  50. package/lib/module/components/tabs/host/TabsHost.ios.js.map +1 -1
  51. package/lib/module/fabric/gamma/stack/StackHeaderConfigAndroidNativeComponent.js.map +1 -1
  52. package/lib/module/flags.js +1 -0
  53. package/lib/module/flags.js.map +1 -1
  54. package/lib/typescript/components/gamma/split/SplitHost.types.d.ts +1 -1
  55. package/lib/typescript/components/gamma/split/SplitHost.types.d.ts.map +1 -1
  56. package/lib/typescript/components/gamma/stack/header/StackHeaderConfig.android.d.ts.map +1 -1
  57. package/lib/typescript/components/gamma/stack/header/StackHeaderConfig.android.types.d.ts +183 -8
  58. package/lib/typescript/components/gamma/stack/header/StackHeaderConfig.android.types.d.ts.map +1 -1
  59. package/lib/typescript/components/gamma/stack/header/StackHeaderConfig.types.d.ts +37 -0
  60. package/lib/typescript/components/gamma/stack/header/StackHeaderConfig.types.d.ts.map +1 -1
  61. package/lib/typescript/components/gamma/stack/header/android/StackHeaderSubview.android.types.d.ts +1 -1
  62. package/lib/typescript/components/gamma/stack/header/android/StackHeaderSubview.android.types.d.ts.map +1 -1
  63. package/lib/typescript/components/gamma/stack/host/StackHost.types.d.ts +1 -1
  64. package/lib/typescript/components/gamma/stack/host/StackHost.types.d.ts.map +1 -1
  65. package/lib/typescript/components/safe-area/SafeAreaView.web.d.ts +1 -1
  66. package/lib/typescript/components/safe-area/SafeAreaView.web.d.ts.map +1 -1
  67. package/lib/typescript/components/tabs/host/TabsHost.types.d.ts +29 -19
  68. package/lib/typescript/components/tabs/host/TabsHost.types.d.ts.map +1 -1
  69. package/lib/typescript/components/tabs/index.d.ts +1 -1
  70. package/lib/typescript/components/tabs/index.d.ts.map +1 -1
  71. package/lib/typescript/fabric/gamma/stack/StackHeaderConfigAndroidNativeComponent.d.ts +5 -0
  72. package/lib/typescript/fabric/gamma/stack/StackHeaderConfigAndroidNativeComponent.d.ts.map +1 -1
  73. package/lib/typescript/fabric/tabs/TabsHostAndroidNativeComponent.d.ts +5 -5
  74. package/lib/typescript/fabric/tabs/TabsHostAndroidNativeComponent.d.ts.map +1 -1
  75. package/lib/typescript/fabric/tabs/TabsHostIOSNativeComponent.d.ts +5 -5
  76. package/lib/typescript/fabric/tabs/TabsHostIOSNativeComponent.d.ts.map +1 -1
  77. package/lib/typescript/flags.d.ts +1 -0
  78. package/lib/typescript/flags.d.ts.map +1 -1
  79. package/package.json +1 -1
  80. package/src/components/gamma/split/SplitHost.types.ts +1 -1
  81. package/src/components/gamma/stack/header/StackHeaderConfig.android.tsx +72 -2
  82. package/src/components/gamma/stack/header/StackHeaderConfig.android.types.ts +183 -8
  83. package/src/components/gamma/stack/header/StackHeaderConfig.types.ts +37 -0
  84. package/src/components/gamma/stack/header/android/StackHeaderSubview.android.types.ts +1 -1
  85. package/src/components/gamma/stack/host/StackHost.types.ts +1 -1
  86. package/src/components/safe-area/SafeAreaView.web.tsx +1 -1
  87. package/src/components/tabs/host/TabsHost.android.tsx +2 -2
  88. package/src/components/tabs/host/TabsHost.ios.tsx +2 -2
  89. package/src/components/tabs/host/TabsHost.types.ts +29 -19
  90. package/src/components/tabs/index.ts +1 -1
  91. package/src/fabric/gamma/stack/StackHeaderConfigAndroidNativeComponent.ts +6 -0
  92. package/src/fabric/tabs/TabsHostAndroidNativeComponent.ts +5 -5
  93. package/src/fabric/tabs/TabsHostIOSNativeComponent.ts +5 -5
  94. package/src/flags.ts +1 -0
  95. package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsContainerOps.kt +0 -7
  96. package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsNavState.kt +0 -43
@@ -53,7 +53,7 @@ namespace react = facebook::react;
53
53
  BOOL _hasModifiedBottomAccessoryInCurrentTransation;
54
54
  BOOL _needsTabBarAppearanceUpdate;
55
55
 
56
- RNSTabsNavigationState *_Nullable _jsNavState;
56
+ RNSTabsNavigationStateUpdateRequest *_Nullable _navStateRequest;
57
57
  }
58
58
 
59
59
  - (instancetype)initWithFrame:(CGRect)frame
@@ -85,6 +85,8 @@ namespace react = facebook::react;
85
85
  [self resetProps];
86
86
 
87
87
  _controller = [[RNSTabBarController alloc] initWithTabsHostComponentView:self];
88
+ RCTAssert([_controller addNavigationStateObserver:self],
89
+ @"[RNScreens] Failed to register RNSTabsHostComponentView as navigation state observer");
88
90
 
89
91
  _reactSubviews = [NSMutableArray new];
90
92
  _reactEventEmitter = [RNSTabsHostEventEmitter new];
@@ -119,6 +121,7 @@ namespace react = facebook::react;
119
121
  dispatch_async(dispatch_get_main_queue(), ^{
120
122
  auto strongSelf = weakSelf;
121
123
  if (strongSelf) {
124
+ [strongSelf->_controller tearDown];
122
125
  strongSelf->_controller = nil;
123
126
  }
124
127
  });
@@ -268,14 +271,16 @@ namespace react = facebook::react;
268
271
  const auto &oldComponentProps = *std::static_pointer_cast<const react::RNSTabsHostIOSProps>(_props);
269
272
  const auto &newComponentProps = *std::static_pointer_cast<const react::RNSTabsHostIOSProps>(props);
270
273
 
271
- if (newComponentProps.navState.selectedScreenKey != oldComponentProps.navState.selectedScreenKey ||
272
- newComponentProps.navState.provenance != oldComponentProps.navState.provenance) {
273
- NSString *selectedScreenKey = RCTNSStringFromStringNilIfEmpty(newComponentProps.navState.selectedScreenKey);
274
+ if (newComponentProps.navStateRequest.selectedScreenKey != oldComponentProps.navStateRequest.selectedScreenKey ||
275
+ newComponentProps.navStateRequest.baseProvenance != oldComponentProps.navStateRequest.baseProvenance) {
276
+ NSString *selectedScreenKey = RCTNSStringFromStringNilIfEmpty(newComponentProps.navStateRequest.selectedScreenKey);
274
277
  RCTAssert(selectedScreenKey != nil, @"[RNScreens] selectedScreenKey MUST NOT be nil");
275
- RCTAssert(newComponentProps.navState.provenance >= 0, @"[RNScreens] provenance MUST BE >= 0]");
276
- _jsNavState = [RNSTabsNavigationState stateWithSelectedScreenKey:selectedScreenKey
277
- provenance:newComponentProps.navState.provenance];
278
- [_controller setPendingNavigationStateUpdate:[_jsNavState cloneState]];
278
+ RCTAssert(newComponentProps.navStateRequest.baseProvenance >= 0, @"[RNScreens] baseProvenance MUST BE >= 0");
279
+ _navStateRequest = [RNSTabsNavigationStateUpdateRequest
280
+ requestWithSelectedScreenKey:selectedScreenKey
281
+ baseProvenance:newComponentProps.navStateRequest.baseProvenance
282
+ actionOrigin:RNSTabsActionOriginProgrammaticJs];
283
+ [_controller setPendingNavigationStateUpdate:[_navStateRequest cloneRequest]];
279
284
  }
280
285
 
281
286
  if (newComponentProps.rejectStaleNavStateUpdates != oldComponentProps.rejectStaleNavStateUpdates) {
@@ -596,11 +601,11 @@ RNS_IGNORE_SUPER_CALL_END
596
601
  return _imageLoader;
597
602
  }
598
603
 
599
- #pragma mark - RNSTabBarControllerDelegate
604
+ #pragma mark - RNSTabsNavigationStateObserver
600
605
 
601
- - (void)tabBarController:(nonnull RNSTabBarController *)tabBarController
602
- didUpdateStateTo:(nonnull RNSTabsNavigationState *)navState
603
- withContext:(nonnull RNSTabsNavigationStateUpdateContext *)context
606
+ - (void)tabsContainer:(nonnull RNSTabBarController *)tabsContainer
607
+ didUpdateStateTo:(nonnull RNSTabsNavigationState *)navState
608
+ withContext:(nonnull RNSTabsNavigationStateUpdateContext *)context
604
609
  {
605
610
  RCTAssert(navState.selectedScreenKey != nil, @"[RNScreens] screenKey MUST NOT be nil");
606
611
 
@@ -608,45 +613,44 @@ RNS_IGNORE_SUPER_CALL_END
608
613
  .provenance = navState.provenance,
609
614
  .isRepeated = context.isRepeated,
610
615
  .hasTriggeredSpecialEffect = context.hasTriggeredSpecialEffect,
611
- .isNativeAction = context.isNativeAction}];
616
+ .actionOrigin = context.actionOrigin}];
612
617
  }
613
618
 
614
- - (void)tabBarController:(nonnull RNSTabBarController *)tabBarController
615
- rejectedStateUpdateTo:(nonnull RNSTabsNavigationState *)rejectedNavState
616
- currentState:(nonnull RNSTabsNavigationState *)currentNavState
617
- withReason:(RNSTabsNavigationStateRejectionReason)reasonCode
619
+ - (void)tabsContainer:(nonnull RNSTabBarController *)tabsContainer
620
+ rejectedStateUpdate:(nonnull RNSTabsNavigationStateUpdateRequest *)rejectedRequest
621
+ currentState:(nonnull RNSTabsNavigationState *)currentNavState
622
+ withReason:(RNSTabsNavigationStateRejectionReason)reason
618
623
  {
619
624
  RCTAssert(currentNavState.selectedScreenKey != nil, @"[RNScreens] Current state screenKey MUST NOT be nil");
620
- RCTAssert(rejectedNavState.selectedScreenKey != nil, @"[RNScreens] Rejected state screenKey MUST NOT be nil");
625
+ RCTAssert(rejectedRequest.selectedScreenKey != nil,
626
+ @"[RNScreens] Rejected request selectedScreenKey MUST NOT be nil");
621
627
 
622
628
  [self.reactEventEmitter emitOnTabSelectionRejected:{.currentNavState = currentNavState,
623
- .rejectedNavState = rejectedNavState,
624
- .rejectionReason = reasonCode}];
629
+ .rejectedRequest = rejectedRequest,
630
+ .rejectionReason = reason}];
625
631
  }
626
632
 
627
- - (void)tabBarController:(nonnull RNSTabBarController *)tabBarController
628
- preventedSelectionOf:(nonnull NSString *)screenKey
633
+ - (void)tabsContainer:(nonnull RNSTabBarController *)tabsContainer
634
+ preventedSelectionOf:(nonnull NSString *)preventedScreenKey
629
635
  currentState:(nonnull RNSTabsNavigationState *)currentNavState
630
636
  {
631
- RCTAssert(tabBarController != nil, @"[RNScreens] Expected NON NIL tabBarController");
632
- RCTAssert(screenKey != nil, @"[RNScreens] Expected NON NIL screenKey");
633
- RCTAssert(
634
- currentNavState != nil && currentNavState.selectedScreenKey != nil,
635
- @"[RNScreens] Expected NON NIL nav state & selectedScreenKey");
637
+ RCTAssert(tabsContainer != nil, @"[RNScreens] Expected NON NIL tabsContainer");
638
+ RCTAssert(preventedScreenKey != nil, @"[RNScreens] Expected NON NIL preventedScreenKey");
639
+ RCTAssert(currentNavState != nil && currentNavState.selectedScreenKey != nil,
640
+ @"[RNScreens] Expected NON NIL nav state & selectedScreenKey");
636
641
 
637
642
  [self.reactEventEmitter emitOnTabSelectionPrevented:{
638
643
  .currentNavState = currentNavState,
639
- .preventedScreenKey = screenKey,
644
+ .preventedScreenKey = preventedScreenKey,
640
645
  }];
641
646
  }
642
647
 
643
- - (void)tabBarController:(nonnull RNSTabBarController *)tabBarController
648
+ - (void)tabsContainer:(nonnull RNSTabBarController *)tabsContainer
644
649
  didSelectMoreTabWithCurrentState:(nonnull RNSTabsNavigationState *)currentNavState
645
650
  {
646
- RCTAssert(tabBarController != nil, @"[RNScreens] Expected NON NIL tabBarController");
647
- RCTAssert(
648
- currentNavState != nil && currentNavState.selectedScreenKey != nil,
649
- @"[RNScreens] Expected NON NIL nav state & selectedScreenKey");
651
+ RCTAssert(tabsContainer != nil, @"[RNScreens] Expected NON NIL tabsContainer");
652
+ RCTAssert(currentNavState != nil && currentNavState.selectedScreenKey != nil,
653
+ @"[RNScreens] Expected NON NIL nav state & selectedScreenKey");
650
654
 
651
655
  [self.reactEventEmitter emitOnMoreTabSelected:{
652
656
  .currentNavState = currentNavState,
@@ -26,16 +26,16 @@ typedef struct {
26
26
  BOOL isRepeated;
27
27
  /** Whether a special effect (e.g. scroll-to-top) was triggered. */
28
28
  BOOL hasTriggeredSpecialEffect;
29
- /** Whether the selection was initiated by a native user action (tap). */
30
- BOOL isNativeAction;
29
+ /** Origin (actor) that requested this transition. */
30
+ RNSTabsActionOrigin actionOrigin;
31
31
  } OnTabSelectedPayload;
32
32
 
33
33
  /** Payload for the `onTabSelectionRejected` event emitted when a tab selection request is rejected. */
34
34
  typedef struct {
35
35
  /** The currently active navigation state that was kept. */
36
36
  RNSTabsNavigationState *_Nonnull currentNavState;
37
- /** The navigation state update that was rejected. */
38
- RNSTabsNavigationState *_Nonnull rejectedNavState;
37
+ /** The navigation state update request that was rejected. */
38
+ RNSTabsNavigationStateUpdateRequest *_Nonnull rejectedRequest;
39
39
  /** Reason the update was rejected. */
40
40
  RNSTabsNavigationStateRejectionReason rejectionReason;
41
41
  } OnTabSelectionRejectedPayload;
@@ -37,12 +37,14 @@ namespace react = facebook::react;
37
37
  - (BOOL)emitOnTabSelected:(OnTabSelectedPayload)payload
38
38
  {
39
39
  if (_reactEventEmitter != nullptr) {
40
+ auto convertedActionOrigin =
41
+ rnscreens::conversion::RNSOnTabSelectedActionOriginFromRNSTabsActionOrigin(payload.actionOrigin);
40
42
  _reactEventEmitter->onTabSelected(
41
43
  {.selectedScreenKey = RCTStringFromNSString(payload.selectedScreenKey),
42
44
  .provenance = payload.provenance,
43
45
  .isRepeated = static_cast<bool>(payload.isRepeated),
44
46
  .hasTriggeredSpecialEffect = static_cast<bool>(payload.hasTriggeredSpecialEffect),
45
- .isNativeAction = static_cast<bool>(payload.isNativeAction)});
47
+ .actionOrigin = convertedActionOrigin});
46
48
  return YES;
47
49
  } else {
48
50
  RCTLogWarn(@"[RNScreens] Skipped OnTabSelected event emission due to nullish emitter");
@@ -59,8 +61,8 @@ namespace react = facebook::react;
59
61
  _reactEventEmitter->onTabSelectionRejected(
60
62
  {.selectedScreenKey = RCTStringFromNSString(payload.currentNavState.selectedScreenKey),
61
63
  .provenance = payload.currentNavState.provenance,
62
- .rejectedScreenKey = RCTStringFromNSString(payload.rejectedNavState.selectedScreenKey),
63
- .rejectedProvenance = payload.rejectedNavState.provenance,
64
+ .rejectedScreenKey = RCTStringFromNSString(payload.rejectedRequest.selectedScreenKey),
65
+ .rejectedBaseProvenance = payload.rejectedRequest.baseProvenance,
64
66
  .rejectionReason = convertedReason});
65
67
  return YES;
66
68
  } else {
@@ -4,6 +4,47 @@
4
4
 
5
5
  NS_ASSUME_NONNULL_BEGIN
6
6
 
7
+ @class RNSTabBarController;
8
+
9
+ /**
10
+ * Origin (actor) that requested a tab transition. Mirrors the public `actionOrigin` event field.
11
+ */
12
+ typedef NS_ENUM(NSInteger, RNSTabsActionOrigin) {
13
+ /**
14
+ * Direct native UI interaction (tab bar tap, drag-and-drop).
15
+ */
16
+ RNSTabsActionOriginUser = 0,
17
+ /**
18
+ * JS-initiated request delivered via the `navStateRequest` prop.
19
+ */
20
+ RNSTabsActionOriginProgrammaticJs,
21
+ /**
22
+ * Request initiated from the native side by some actor, e.g. a downstream
23
+ * library integrating directly against `RNSTabBarController`.
24
+ */
25
+ RNSTabsActionOriginProgrammaticNative,
26
+ /**
27
+ * Platform side effect not attributable to an explicit actor — UIKit changed the selection
28
+ * as a side effect of another operation (e.g. More navigation controller disappearing during a
29
+ * horizontal size class transition on iPad).
30
+ */
31
+ RNSTabsActionOriginImplicit,
32
+ };
33
+
34
+ /**
35
+ * Reason why a navigation state update was rejected by the container.
36
+ */
37
+ typedef NS_ENUM(NSInteger, RNSTabsNavigationStateRejectionReason) {
38
+ /**
39
+ * The update's provenance is based on a stale state.
40
+ */
41
+ RNSTabsNavigationStateRejectionReasonStale = 0,
42
+ /**
43
+ * The requested tab is already selected.
44
+ */
45
+ RNSTabsNavigationStateRejectionReasonRepeated,
46
+ };
47
+
7
48
  /**
8
49
  * Describes navigation state of a tabs container.
9
50
  *
@@ -13,11 +54,15 @@ NS_ASSUME_NONNULL_BEGIN
13
54
  */
14
55
  @interface RNSTabsNavigationState : NSObject
15
56
 
16
- /** Screen key of the currently selected tab. */
57
+ /**
58
+ * Screen key of the currently selected tab.
59
+ */
17
60
  @property (nonatomic, strong, readonly, nonnull) NSString *selectedScreenKey;
18
61
 
19
- /** Monotonically increasing number describing the generation of this state instance.
20
- * Used for stale update detection: state A is stale iff A.provenance <= B.provenance. */
62
+ /**
63
+ * Monotonically increasing number describing the generation of this state instance.
64
+ * Used for stale update detection.
65
+ */
21
66
  @property (nonatomic, readonly) int provenance;
22
67
 
23
68
  - (instancetype)initWithSelectedScreenKey:(nonnull NSString *)selectedScreenKey provenance:(int)provenance;
@@ -28,42 +73,112 @@ NS_ASSUME_NONNULL_BEGIN
28
73
 
29
74
  @end
30
75
 
31
- /** Bundles a navigation state change together with metadata about the selection context. */
76
+ /**
77
+ * A request to change navigation state.
78
+ *
79
+ * Carries the target `selectedScreenKey`, the `baseProvenance` of the state the request was derived from,
80
+ * and the `actionOrigin` (actor) that initiated it. Mirrors the public
81
+ * `TabsHostNavStateRequest` TS type plus an `actionOrigin` carried internally.
82
+ */
83
+ @interface RNSTabsNavigationStateUpdateRequest : NSObject
84
+
85
+ @property (nonatomic, strong, readonly, nonnull) NSString *selectedScreenKey;
86
+ @property (nonatomic, readonly) int baseProvenance;
87
+ @property (nonatomic, readonly) RNSTabsActionOrigin actionOrigin;
88
+
89
+ - (instancetype)initWithSelectedScreenKey:(nonnull NSString *)selectedScreenKey
90
+ baseProvenance:(int)baseProvenance
91
+ actionOrigin:(RNSTabsActionOrigin)actionOrigin;
92
+
93
+ - (instancetype)cloneRequest;
94
+
95
+ + (instancetype)requestWithSelectedScreenKey:(nonnull NSString *)selectedScreenKey
96
+ baseProvenance:(int)baseProvenance
97
+ actionOrigin:(RNSTabsActionOrigin)actionOrigin;
98
+
99
+ @end
100
+
101
+ /**
102
+ * Bundles a navigation state change together with metadata about the selection context.
103
+ */
32
104
  @interface RNSTabsNavigationStateUpdateContext : NSObject
33
105
 
34
- /** The navigation state after the change. */
106
+ /**
107
+ * The navigation state after the change.
108
+ */
35
109
  @property (nonatomic, readonly, strong, nonnull) RNSTabsNavigationState *navState;
36
- /** Whether the same tab that was already selected has been selected again. */
110
+ /**
111
+ * Whether the same tab that was already selected has been selected again.
112
+ */
37
113
  @property (nonatomic, readonly) BOOL isRepeated;
38
- /** Whether a special effect (e.g. scroll-to-top) was triggered by the selection. */
114
+ /**
115
+ * Whether a special effect (e.g. scroll-to-top) was triggered by the selection.
116
+ */
39
117
  @property (nonatomic, readonly) BOOL hasTriggeredSpecialEffect;
40
- /** Whether the selection was initiated by a native user action (tap) as opposed to a JS-driven update. */
41
- @property (nonatomic, readonly) BOOL isNativeAction;
118
+ /**
119
+ * Origin (actor) that requested this transition.
120
+ */
121
+ @property (nonatomic, readonly) RNSTabsActionOrigin actionOrigin;
42
122
 
43
123
  - (instancetype)initWithNavState:(nonnull RNSTabsNavigationState *)navState
44
124
  isRepeated:(BOOL)isRepeated
45
125
  hasTriggeredSpecialEffect:(BOOL)hasTriggeredSpecialEffect
46
- isNativeAction:(BOOL)isNativeAction;
126
+ actionOrigin:(RNSTabsActionOrigin)actionOrigin;
47
127
 
48
128
  @end
49
129
 
50
- /** Source of a navigation state update. */
51
- typedef NS_ENUM(NSInteger, RNSTabsNavigationStateUpdateSource) {
52
- /** Update initiated by a native user interaction (e.g. tab tap). */
53
- RNSTabsNavigationStateUpdateSourceUser = 0,
54
- /** Update initiated externally (e.g. from JS via props). */
55
- RNSTabsNavigationStateUpdateSourceExternal,
56
- /** Update detected implicitly UIKit changed the selection as a side effect of another operation
57
- * (e.g. More navigation controller disappearing during a horizontal size class transition on iPad). */
58
- RNSTabsNavigationStateUpdateSourceImplicit
59
- };
130
+ /**
131
+ * Observer of navigation state changes on a tabs container (`RNSTabBarController`).
132
+ *
133
+ * Multiple observers may register against a single container via
134
+ * `RNSTabBarController addNavigationStateObserver:` / `removeNavigationStateObserver:`.
135
+ * The host (`RNSTabsHostComponentView`) registers itself as an observer to relay events
136
+ * to JS; downstream native libraries integrating directly against `RNSTabBarController`
137
+ * may register additional observers.
138
+ *
139
+ * Observers are held with strong references; callers must explicitly call
140
+ * `removeNavigationStateObserver:` before observer dealloc, or rely on the host
141
+ * invoking `tearDown` (which clears the registry) on container teardown.
142
+ */
143
+ @protocol RNSTabsNavigationStateObserver <NSObject>
60
144
 
61
- /** Reason why a navigation state update was rejected by the container. */
62
- typedef NS_ENUM(NSInteger, RNSTabsNavigationStateRejectionReason) {
63
- /** The update's provenance is based on a stale state. */
64
- RNSTabsNavigationStateRejectionReasonStale = 0,
65
- /** The requested tab is already selected. */
66
- RNSTabsNavigationStateRejectionReasonRepeated,
67
- };
145
+ @required
146
+
147
+ /**
148
+ * Called when the container accepts a navigation state change.
149
+ */
150
+ - (void)tabsContainer:(nonnull RNSTabBarController *)tabsContainer
151
+ didUpdateStateTo:(nonnull RNSTabsNavigationState *)navState
152
+ withContext:(nonnull RNSTabsNavigationStateUpdateContext *)context;
153
+
154
+ /**
155
+ * Called when the container rejects a navigation state update.
156
+ */
157
+ - (void)tabsContainer:(nonnull RNSTabBarController *)tabsContainer
158
+ rejectedStateUpdate:(nonnull RNSTabsNavigationStateUpdateRequest *)rejectedRequest
159
+ currentState:(nonnull RNSTabsNavigationState *)currentNavState
160
+ withReason:(RNSTabsNavigationStateRejectionReason)reason;
161
+
162
+ /**
163
+ * Called when a native user action (tap) attempts to select a tab that has
164
+ * `preventNativeSelection` enabled. The navigation state remains unchanged.
165
+ */
166
+ - (void)tabsContainer:(nonnull RNSTabBarController *)tabsContainer
167
+ preventedSelectionOf:(nonnull NSString *)preventedScreenKey
168
+ currentState:(nonnull RNSTabsNavigationState *)currentNavState;
169
+
170
+ @optional
171
+
172
+ /**
173
+ * iOS-only. Called when the user taps the More tab on iPhone or in iPad split view
174
+ * configurations where UIKit groups overflow tabs into a More navigation controller.
175
+ *
176
+ * This event is informational; the navigation state is not advanced as part of this callback.
177
+ * Android does not emit an analogue.
178
+ */
179
+ - (void)tabsContainer:(nonnull RNSTabBarController *)tabsContainer
180
+ didSelectMoreTabWithCurrentState:(nonnull RNSTabsNavigationState *)currentNavState;
181
+
182
+ @end
68
183
 
69
184
  NS_ASSUME_NONNULL_END
@@ -26,18 +26,51 @@
26
26
 
27
27
  @end
28
28
 
29
+ @implementation RNSTabsNavigationStateUpdateRequest
30
+
31
+ - (instancetype)initWithSelectedScreenKey:(nonnull NSString *)selectedScreenKey
32
+ baseProvenance:(int)baseProvenance
33
+ actionOrigin:(RNSTabsActionOrigin)actionOrigin
34
+ {
35
+ if (self = [super init]) {
36
+ _selectedScreenKey = selectedScreenKey;
37
+ _baseProvenance = baseProvenance;
38
+ _actionOrigin = actionOrigin;
39
+ }
40
+ return self;
41
+ }
42
+
43
+ - (instancetype)cloneRequest
44
+ {
45
+ return [[RNSTabsNavigationStateUpdateRequest alloc]
46
+ initWithSelectedScreenKey:[NSString stringWithString:self.selectedScreenKey]
47
+ baseProvenance:self.baseProvenance
48
+ actionOrigin:self.actionOrigin];
49
+ }
50
+
51
+ + (instancetype)requestWithSelectedScreenKey:(nonnull NSString *)selectedScreenKey
52
+ baseProvenance:(int)baseProvenance
53
+ actionOrigin:(RNSTabsActionOrigin)actionOrigin
54
+ {
55
+ return [[RNSTabsNavigationStateUpdateRequest alloc] initWithSelectedScreenKey:selectedScreenKey
56
+ baseProvenance:baseProvenance
57
+ actionOrigin:actionOrigin];
58
+ }
59
+
60
+ @end
61
+
29
62
  @implementation RNSTabsNavigationStateUpdateContext
30
63
 
31
64
  - (instancetype)initWithNavState:(nonnull RNSTabsNavigationState *)navState
32
65
  isRepeated:(BOOL)isRepeated
33
66
  hasTriggeredSpecialEffect:(BOOL)hasTriggeredSpecialEffect
34
- isNativeAction:(BOOL)isNativeAction
67
+ actionOrigin:(RNSTabsActionOrigin)actionOrigin
35
68
  {
36
69
  if (self = [super init]) {
37
70
  _navState = navState;
38
71
  _isRepeated = isRepeated;
39
72
  _hasTriggeredSpecialEffect = hasTriggeredSpecialEffect;
40
- _isNativeAction = isNativeAction;
73
+ _actionOrigin = actionOrigin;
41
74
  }
42
75
  return self;
43
76
  }
@@ -0,0 +1,62 @@
1
+ #pragma once
2
+
3
+ #import <Foundation/Foundation.h>
4
+ #import "RNSTabsNavigationState.h"
5
+
6
+ NS_ASSUME_NONNULL_BEGIN
7
+
8
+ /**
9
+ * Holds the set of `RNSTabsNavigationStateObserver`s registered against an
10
+ * `RNSTabBarController` and fans out container events to all of them.
11
+ *
12
+ * Observers are held with strong references; callers must explicitly call
13
+ * `removeObserver:` before observer dealloc, or rely on the host invoking
14
+ * `clear` (via `RNSTabBarController.tearDown`) on container teardown.
15
+ *
16
+ * `emit*` methods invoke each observer in registration order. The `@optional`
17
+ * More-tab callback is dispatched only to observers that respond to the selector.
18
+ */
19
+ @interface RNSTabsNavigationStateObserverRegistry : NSObject
20
+
21
+ /**
22
+ * Register an observer.
23
+ *
24
+ * @returns NO if the observer is already registered or if called
25
+ * during an in-flight `emit*` (modifications during emission are rejected).
26
+ */
27
+ - (BOOL)addObserver:(id<RNSTabsNavigationStateObserver>)observer;
28
+
29
+ /**
30
+ * Unregister an observer.
31
+ *
32
+ * @returns NO if the observer was not registered or if called
33
+ * during an in-flight `emit*` (modifications during emission are rejected).
34
+ */
35
+ - (BOOL)removeObserver:(id<RNSTabsNavigationStateObserver>)observer;
36
+
37
+ /**
38
+ * Drop all registered observers.
39
+ *
40
+ * @returns NO if called during in-flight `emit*` (modifications during emission are rejected).
41
+ */
42
+ - (BOOL)clear;
43
+
44
+ - (void)emitDidUpdateStateTo:(nonnull RNSTabsNavigationState *)navState
45
+ withContext:(nonnull RNSTabsNavigationStateUpdateContext *)context
46
+ sender:(nonnull RNSTabBarController *)sender;
47
+
48
+ - (void)emitRejectedStateUpdate:(nonnull RNSTabsNavigationStateUpdateRequest *)rejectedRequest
49
+ currentState:(nonnull RNSTabsNavigationState *)currentNavState
50
+ withReason:(RNSTabsNavigationStateRejectionReason)reason
51
+ sender:(nonnull RNSTabBarController *)sender;
52
+
53
+ - (void)emitPreventedSelectionOf:(nonnull NSString *)preventedScreenKey
54
+ currentState:(nonnull RNSTabsNavigationState *)currentNavState
55
+ sender:(nonnull RNSTabBarController *)sender;
56
+
57
+ - (void)emitDidSelectMoreTabWithCurrentState:(nonnull RNSTabsNavigationState *)currentNavState
58
+ sender:(nonnull RNSTabBarController *)sender;
59
+
60
+ @end
61
+
62
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,104 @@
1
+ #import "RNSTabsNavigationStateObserverRegistry.h"
2
+
3
+ #import <React/RCTAssert.h>
4
+
5
+ #import "RNSTabBarController.h"
6
+
7
+ @implementation RNSTabsNavigationStateObserverRegistry {
8
+ NSMutableArray<id<RNSTabsNavigationStateObserver>> *_observers;
9
+ BOOL _isEmitting;
10
+ }
11
+
12
+ - (instancetype)init
13
+ {
14
+ if (self = [super init]) {
15
+ _observers = [NSMutableArray new];
16
+ _isEmitting = NO;
17
+ }
18
+ return self;
19
+ }
20
+
21
+ - (BOOL)addObserver:(id<RNSTabsNavigationStateObserver>)observer
22
+ {
23
+ if (_isEmitting || [_observers containsObject:observer]) {
24
+ return NO;
25
+ }
26
+ [_observers addObject:observer];
27
+ return YES;
28
+ }
29
+
30
+ - (BOOL)removeObserver:(id<RNSTabsNavigationStateObserver>)observer
31
+ {
32
+ if (_isEmitting || ![_observers containsObject:observer]) {
33
+ return NO;
34
+ }
35
+ [_observers removeObject:observer];
36
+ return YES;
37
+ }
38
+
39
+ - (BOOL)clear
40
+ {
41
+ if (_isEmitting) {
42
+ return NO;
43
+ }
44
+ [_observers removeAllObjects];
45
+ return YES;
46
+ }
47
+
48
+ - (void)emitDidUpdateStateTo:(nonnull RNSTabsNavigationState *)navState
49
+ withContext:(nonnull RNSTabsNavigationStateUpdateContext *)context
50
+ sender:(nonnull RNSTabBarController *)sender
51
+ {
52
+ [self emitSignal:^(id<RNSTabsNavigationStateObserver> observer) {
53
+ [observer tabsContainer:sender didUpdateStateTo:navState withContext:context];
54
+ }];
55
+ }
56
+
57
+ - (void)emitRejectedStateUpdate:(nonnull RNSTabsNavigationStateUpdateRequest *)rejectedRequest
58
+ currentState:(nonnull RNSTabsNavigationState *)currentNavState
59
+ withReason:(RNSTabsNavigationStateRejectionReason)reason
60
+ sender:(nonnull RNSTabBarController *)sender
61
+ {
62
+ [self emitSignal:^(id<RNSTabsNavigationStateObserver> observer) {
63
+ [observer tabsContainer:sender rejectedStateUpdate:rejectedRequest currentState:currentNavState withReason:reason];
64
+ }];
65
+ }
66
+
67
+ - (void)emitPreventedSelectionOf:(nonnull NSString *)preventedScreenKey
68
+ currentState:(nonnull RNSTabsNavigationState *)currentNavState
69
+ sender:(nonnull RNSTabBarController *)sender
70
+ {
71
+ [self emitSignal:^(id<RNSTabsNavigationStateObserver> observer) {
72
+ [observer tabsContainer:sender preventedSelectionOf:preventedScreenKey currentState:currentNavState];
73
+ }];
74
+ }
75
+
76
+ - (void)emitDidSelectMoreTabWithCurrentState:(nonnull RNSTabsNavigationState *)currentNavState
77
+ sender:(nonnull RNSTabBarController *)sender
78
+ {
79
+ SEL observerSelector = @selector(tabsContainer:didSelectMoreTabWithCurrentState:);
80
+ [self emitSignal:^(id<RNSTabsNavigationStateObserver> observer) {
81
+ if ([observer respondsToSelector:observerSelector]) {
82
+ [observer tabsContainer:sender didSelectMoreTabWithCurrentState:currentNavState];
83
+ }
84
+ }];
85
+ }
86
+
87
+ - (void)emitSignal:(void (^)(id<RNSTabsNavigationStateObserver> observer))emitBlock
88
+ {
89
+ RCTAssert(!_isEmitting, @"[RNScreens] Recursive emission on RNSTabsNavigationStateObserverRegistry");
90
+
91
+ // In release best we can do here is let it emit the event. Alternatives are to crash or not emit (certain wrong
92
+ // state).
93
+
94
+ _isEmitting = YES;
95
+ @try {
96
+ for (id<RNSTabsNavigationStateObserver> observer in _observers) {
97
+ emitBlock(observer);
98
+ }
99
+ } @finally {
100
+ _isEmitting = NO;
101
+ }
102
+ }
103
+
104
+ @end
@@ -27,13 +27,25 @@ function StackHeaderConfig(props) {
27
27
  centerSubview,
28
28
  trailingSubview,
29
29
  backButtonIcon,
30
+ scrollFlagScroll,
31
+ scrollFlagEnterAlways,
32
+ scrollFlagEnterAlwaysCollapsed,
33
+ scrollFlagExitUntilCollapsed,
34
+ scrollFlagSnap,
30
35
  ...filteredAndroidProps
31
36
  } = android ?? {};
32
37
  const backButtonIconProps = parseBackButtonIconToNativeProps(backButtonIcon);
38
+ const scrollFlagProps = resolveScrollFlags(filteredAndroidProps.type, {
39
+ scrollFlagScroll,
40
+ scrollFlagEnterAlways,
41
+ scrollFlagEnterAlwaysCollapsed,
42
+ scrollFlagExitUntilCollapsed,
43
+ scrollFlagSnap
44
+ });
33
45
  return /*#__PURE__*/_react.default.createElement(_StackHeaderConfigAndroidNativeComponent.default, _extends({
34
46
  collapsable: false,
35
47
  style: _reactNative.StyleSheet.absoluteFill
36
- }, baseProps, filteredAndroidProps, backButtonIconProps), backgroundSubview && /*#__PURE__*/_react.default.createElement(_StackHeaderSubview.default, {
48
+ }, baseProps, filteredAndroidProps, backButtonIconProps, scrollFlagProps), backgroundSubview && /*#__PURE__*/_react.default.createElement(_StackHeaderSubview.default, {
37
49
  type: "background",
38
50
  collapseMode: backgroundSubview.collapseMode
39
51
  }, backgroundSubview.Component), leadingSubview && /*#__PURE__*/_react.default.createElement(_StackHeaderSubview.default, {
@@ -64,5 +76,38 @@ function parseBackButtonIconToNativeProps(icon) {
64
76
  throw new Error('[RNScreens] Incorrect icon format for Android. You must provide `imageSource` or `drawableResource`.');
65
77
  }
66
78
  }
79
+ const SCROLL_FLAG_DEFAULTS_BY_TYPE = {
80
+ small: {
81
+ scrollFlagScroll: false,
82
+ scrollFlagEnterAlways: false,
83
+ scrollFlagEnterAlwaysCollapsed: false,
84
+ scrollFlagExitUntilCollapsed: false,
85
+ scrollFlagSnap: false
86
+ },
87
+ medium: {
88
+ scrollFlagScroll: true,
89
+ scrollFlagEnterAlways: false,
90
+ scrollFlagEnterAlwaysCollapsed: false,
91
+ scrollFlagExitUntilCollapsed: true,
92
+ scrollFlagSnap: true
93
+ },
94
+ large: {
95
+ scrollFlagScroll: true,
96
+ scrollFlagEnterAlways: false,
97
+ scrollFlagEnterAlwaysCollapsed: false,
98
+ scrollFlagExitUntilCollapsed: true,
99
+ scrollFlagSnap: true
100
+ }
101
+ };
102
+ function resolveScrollFlags(type, overrides) {
103
+ const defaults = SCROLL_FLAG_DEFAULTS_BY_TYPE[type ?? 'small'];
104
+ return {
105
+ scrollFlagScroll: overrides.scrollFlagScroll ?? defaults.scrollFlagScroll,
106
+ scrollFlagEnterAlways: overrides.scrollFlagEnterAlways ?? defaults.scrollFlagEnterAlways,
107
+ scrollFlagEnterAlwaysCollapsed: overrides.scrollFlagEnterAlwaysCollapsed ?? defaults.scrollFlagEnterAlwaysCollapsed,
108
+ scrollFlagExitUntilCollapsed: overrides.scrollFlagExitUntilCollapsed ?? defaults.scrollFlagExitUntilCollapsed,
109
+ scrollFlagSnap: overrides.scrollFlagSnap ?? defaults.scrollFlagSnap
110
+ };
111
+ }
67
112
  var _default = exports.default = StackHeaderConfig;
68
113
  //# sourceMappingURL=StackHeaderConfig.android.js.map