react-native-navigation 8.8.6 → 8.8.7-snapshot.2601

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 (126) hide show
  1. package/android/src/main/java/com/reactnativenavigation/NavigationApplication.java +3 -0
  2. package/android/src/main/java/com/reactnativenavigation/NavigationPackage.kt +27 -8
  3. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRow.kt +262 -0
  4. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowAttacher.kt +205 -0
  5. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowConfigStore.kt +32 -0
  6. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowLayout.kt +139 -0
  7. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowModule.kt +37 -0
  8. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowOptions.kt +68 -0
  9. package/android/src/main/java/com/reactnativenavigation/options/BottomTabOptions.java +4 -1
  10. package/android/src/main/java/com/reactnativenavigation/react/ReactView.java +13 -0
  11. package/android/src/main/java/com/reactnativenavigation/react/events/ComponentType.java +2 -1
  12. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabPresenter.java +28 -0
  13. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java +59 -0
  14. package/android/src/main/java/com/reactnativenavigation/views/bottomtabs/BottomTabs.java +76 -0
  15. package/android/src/main/java/com/reactnativenavigation/views/bottomtabs/CustomBottomTabItemView.kt +73 -0
  16. package/android/src/main/java/com/reactnativenavigation/views/stack/topbar/titlebar/TitleBarReactButtonView.java +54 -12
  17. package/android/src/test/java/com/reactnativenavigation/views/TitleAndButtonsContainerTest.kt +15 -1
  18. package/android/src/test/java/com/reactnativenavigation/views/TitleBarReactButtonViewTest.java +199 -0
  19. package/ios/ARCHITECTURE.md +5 -0
  20. package/ios/BottomTabPresenter.h +7 -0
  21. package/ios/BottomTabPresenter.mm +27 -0
  22. package/ios/RNNAppDelegate.h +16 -0
  23. package/ios/RNNAppDelegate.mm +73 -0
  24. package/ios/RNNBottomTabOptions.h +2 -0
  25. package/ios/RNNBottomTabOptions.mm +5 -1
  26. package/ios/RNNBottomTabsController.h +2 -0
  27. package/ios/RNNBottomTabsController.mm +209 -1
  28. package/ios/RNNBottomTabsCustomRow.h +57 -0
  29. package/ios/RNNBottomTabsCustomRow.mm +252 -0
  30. package/ios/RNNBottomTabsCustomRowOptions.h +42 -0
  31. package/ios/RNNBottomTabsCustomRowOptions.mm +37 -0
  32. package/ios/RNNBottomTabsOptions.h +2 -0
  33. package/ios/RNNBottomTabsOptions.mm +2 -0
  34. package/ios/RNNComponentViewCreator.h +2 -1
  35. package/ios/RNNCustomTabBarItemView.h +26 -0
  36. package/ios/RNNCustomTabBarItemView.mm +83 -0
  37. package/ios/RNNReactRootViewCreator.mm +1 -0
  38. package/ios/RNNViewControllerFactory.mm +1 -0
  39. package/ios/ReactNativeNavigation.xcodeproj/project.pbxproj +24 -0
  40. package/lib/module/ARCHITECTURE.md +30 -0
  41. package/lib/module/Navigation.js +34 -1
  42. package/lib/module/Navigation.js.map +1 -1
  43. package/lib/module/NavigationDelegate.js +21 -0
  44. package/lib/module/NavigationDelegate.js.map +1 -1
  45. package/lib/module/adapters/AndroidCustomRowForwarder.js +75 -0
  46. package/lib/module/adapters/AndroidCustomRowForwarder.js.map +1 -0
  47. package/lib/module/commands/Commands.js +8 -0
  48. package/lib/module/commands/Commands.js.map +1 -1
  49. package/lib/module/index.js +1 -0
  50. package/lib/module/index.js.map +1 -1
  51. package/lib/module/interfaces/Options.js.map +1 -1
  52. package/lib/module/linking/DeferredLinkQueue.js +52 -0
  53. package/lib/module/linking/DeferredLinkQueue.js.map +1 -0
  54. package/lib/module/linking/DeferredLinkQueue.test.js +54 -0
  55. package/lib/module/linking/DeferredLinkQueue.test.js.map +1 -0
  56. package/lib/module/linking/LinkingHandler.js +139 -0
  57. package/lib/module/linking/LinkingHandler.js.map +1 -0
  58. package/lib/module/linking/LinkingHandler.test.js +384 -0
  59. package/lib/module/linking/LinkingHandler.test.js.map +1 -0
  60. package/lib/module/linking/ModalLayoutBuilder.js +56 -0
  61. package/lib/module/linking/ModalLayoutBuilder.js.map +1 -0
  62. package/lib/module/linking/ModalLayoutBuilder.test.js +154 -0
  63. package/lib/module/linking/ModalLayoutBuilder.test.js.map +1 -0
  64. package/lib/module/linking/RouteMatcher.js +104 -0
  65. package/lib/module/linking/RouteMatcher.js.map +1 -0
  66. package/lib/module/linking/RouteMatcher.test.js +164 -0
  67. package/lib/module/linking/RouteMatcher.test.js.map +1 -0
  68. package/lib/module/linking/URLParser.js +56 -0
  69. package/lib/module/linking/URLParser.js.map +1 -0
  70. package/lib/module/linking/URLParser.test.js +100 -0
  71. package/lib/module/linking/URLParser.test.js.map +1 -0
  72. package/lib/module/linking/types.js +4 -0
  73. package/lib/module/linking/types.js.map +1 -0
  74. package/lib/typescript/Navigation.d.ts +22 -0
  75. package/lib/typescript/Navigation.d.ts.map +1 -1
  76. package/lib/typescript/NavigationDelegate.d.ts +13 -0
  77. package/lib/typescript/NavigationDelegate.d.ts.map +1 -1
  78. package/lib/typescript/adapters/AndroidCustomRowForwarder.d.ts +23 -0
  79. package/lib/typescript/adapters/AndroidCustomRowForwarder.d.ts.map +1 -0
  80. package/lib/typescript/commands/Commands.d.ts +1 -0
  81. package/lib/typescript/commands/Commands.d.ts.map +1 -1
  82. package/lib/typescript/index.d.ts +1 -0
  83. package/lib/typescript/index.d.ts.map +1 -1
  84. package/lib/typescript/interfaces/Options.d.ts +85 -0
  85. package/lib/typescript/interfaces/Options.d.ts.map +1 -1
  86. package/lib/typescript/linking/DeferredLinkQueue.d.ts +26 -0
  87. package/lib/typescript/linking/DeferredLinkQueue.d.ts.map +1 -0
  88. package/lib/typescript/linking/DeferredLinkQueue.test.d.ts +2 -0
  89. package/lib/typescript/linking/DeferredLinkQueue.test.d.ts.map +1 -0
  90. package/lib/typescript/linking/LinkingHandler.d.ts +71 -0
  91. package/lib/typescript/linking/LinkingHandler.d.ts.map +1 -0
  92. package/lib/typescript/linking/LinkingHandler.test.d.ts +2 -0
  93. package/lib/typescript/linking/LinkingHandler.test.d.ts.map +1 -0
  94. package/lib/typescript/linking/ModalLayoutBuilder.d.ts +21 -0
  95. package/lib/typescript/linking/ModalLayoutBuilder.d.ts.map +1 -0
  96. package/lib/typescript/linking/ModalLayoutBuilder.test.d.ts +2 -0
  97. package/lib/typescript/linking/ModalLayoutBuilder.test.d.ts.map +1 -0
  98. package/lib/typescript/linking/RouteMatcher.d.ts +23 -0
  99. package/lib/typescript/linking/RouteMatcher.d.ts.map +1 -0
  100. package/lib/typescript/linking/RouteMatcher.test.d.ts +2 -0
  101. package/lib/typescript/linking/RouteMatcher.test.d.ts.map +1 -0
  102. package/lib/typescript/linking/URLParser.d.ts +16 -0
  103. package/lib/typescript/linking/URLParser.d.ts.map +1 -0
  104. package/lib/typescript/linking/URLParser.test.d.ts +2 -0
  105. package/lib/typescript/linking/URLParser.test.d.ts.map +1 -0
  106. package/lib/typescript/linking/types.d.ts +107 -0
  107. package/lib/typescript/linking/types.d.ts.map +1 -0
  108. package/package.json +1 -1
  109. package/src/ARCHITECTURE.md +30 -0
  110. package/src/Navigation.ts +36 -1
  111. package/src/NavigationDelegate.ts +22 -0
  112. package/src/adapters/AndroidCustomRowForwarder.ts +83 -0
  113. package/src/commands/Commands.ts +15 -0
  114. package/src/index.ts +1 -0
  115. package/src/interfaces/Options.ts +87 -0
  116. package/src/linking/DeferredLinkQueue.test.ts +60 -0
  117. package/src/linking/DeferredLinkQueue.ts +55 -0
  118. package/src/linking/LinkingHandler.test.ts +332 -0
  119. package/src/linking/LinkingHandler.ts +169 -0
  120. package/src/linking/ModalLayoutBuilder.test.ts +105 -0
  121. package/src/linking/ModalLayoutBuilder.ts +60 -0
  122. package/src/linking/RouteMatcher.test.ts +128 -0
  123. package/src/linking/RouteMatcher.ts +126 -0
  124. package/src/linking/URLParser.test.ts +105 -0
  125. package/src/linking/URLParser.ts +62 -0
  126. package/src/linking/types.ts +115 -0
@@ -3,19 +3,22 @@ package com.reactnativenavigation.views.stack.topbar.titlebar;
3
3
  import android.annotation.SuppressLint;
4
4
  import android.content.Context;
5
5
  import android.util.TypedValue;
6
+ import android.view.Gravity;
6
7
  import android.view.View;
8
+ import android.widget.FrameLayout;
7
9
 
8
- import com.facebook.react.ReactInstanceManager;
9
10
  import com.reactnativenavigation.options.ComponentOptions;
10
11
  import com.reactnativenavigation.options.params.Number;
11
12
  import com.reactnativenavigation.react.ReactView;
12
13
 
14
+ import static android.view.View.MeasureSpec.AT_MOST;
13
15
  import static android.view.View.MeasureSpec.EXACTLY;
14
16
  import static android.view.View.MeasureSpec.makeMeasureSpec;
15
17
  import static com.reactnativenavigation.utils.UiUtils.dpToPx;
16
18
 
17
19
  @SuppressLint("ViewConstructor")
18
20
  public class TitleBarReactButtonView extends ReactView {
21
+ private static final float FINAL_WIDTH_PADDING_DP = 1f;
19
22
  private final ComponentOptions component;
20
23
 
21
24
  public TitleBarReactButtonView(Context context, ComponentOptions component) {
@@ -23,28 +26,67 @@ public class TitleBarReactButtonView extends ReactView {
23
26
  this.component = component;
24
27
  }
25
28
 
29
+ @Override
30
+ public void onViewAdded(View child) {
31
+ super.onViewAdded(child);
32
+ if (child.getLayoutParams() instanceof FrameLayout.LayoutParams) {
33
+ FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) child.getLayoutParams();
34
+ layoutParams.gravity = layoutParams.gravity == -1
35
+ ? Gravity.CENTER_VERTICAL
36
+ : (layoutParams.gravity & ~Gravity.VERTICAL_GRAVITY_MASK) | Gravity.CENTER_VERTICAL;
37
+ }
38
+ }
39
+
26
40
  @Override
27
41
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
28
-
29
- //This is a workaround, ReactNative throws exception when views have ids, On android MenuItems
42
+ // This is a workaround, ReactNative throws exception when views have ids, On android MenuItems
30
43
  // With ActionViews like this got an id, see #7253
31
44
  if (!this.isAttachedToWindow()) {
32
45
  this.setId(View.NO_ID);
33
46
  }
34
47
 
35
- super.onMeasure(createSpec(widthMeasureSpec, component.width), createSpec(heightMeasureSpec, component.height));
48
+ int initialWidthSpec = component.width.hasValue()
49
+ ? createExactSpec(component.width)
50
+ : makeMeasureSpec(resolveAvailableWidth(widthMeasureSpec), AT_MOST);
51
+ int initialHeightSpec = createHeightSpec(heightMeasureSpec, component.height);
52
+
53
+ // First discover the content size without forcing every custom button to actionBarSize.
54
+ super.onMeasure(initialWidthSpec, initialHeightSpec);
55
+
56
+ if (component.width.hasValue() && component.height.hasValue()) {
57
+ return;
58
+ }
59
+
60
+ // Then give RN/Yoga a stable exact final box for compatibility with centered button layouts.
61
+ // A small allowance avoids clipping implicit RN padding/subpixel layout while staying content-based.
62
+ int finalWidth = component.width.hasValue()
63
+ ? MeasureSpec.getSize(initialWidthSpec)
64
+ : resolveFinalWidth(getMeasuredWidth());
65
+ int finalHeight = component.height.hasValue()
66
+ ? MeasureSpec.getSize(initialHeightSpec)
67
+ : Math.max(getMeasuredHeight(), 1);
68
+ super.onMeasure(makeMeasureSpec(finalWidth, EXACTLY), makeMeasureSpec(finalHeight, EXACTLY));
36
69
  }
37
70
 
38
- private int createSpec(int measureSpec, Number dimension) {
71
+ private int createHeightSpec(int measureSpec, Number dimension) {
39
72
  if (dimension.hasValue()) {
40
- return makeMeasureSpec(MeasureSpec.getSize(dpToPx(getContext(), dimension.get())), EXACTLY);
41
- } else {
42
- // When JS doesn't pass width/height, default to the theme's actionBarSize (48dp on Material).
43
- // Yoga's intrinsic measurement of the React view collapses `paddingHorizontal` on the
44
- // trailing edge in RTL (RN/Fabric measurement quirk), so we cannot trust UNSPECIFIED here -
45
- // it produces a 0dp visible inset against the screen edge in RTL.
46
- return makeMeasureSpec(resolveActionBarSize(), EXACTLY);
73
+ return createExactSpec(dimension);
47
74
  }
75
+ int availableSize = MeasureSpec.getSize(measureSpec);
76
+ return makeMeasureSpec(availableSize > 0 ? availableSize : Math.max(resolveActionBarSize(), 1), AT_MOST);
77
+ }
78
+
79
+ private int resolveAvailableWidth(int measureSpec) {
80
+ int availableSize = MeasureSpec.getSize(measureSpec);
81
+ return availableSize > 0 ? availableSize : Math.max(getResources().getDisplayMetrics().widthPixels, 1);
82
+ }
83
+
84
+ private int resolveFinalWidth(int measuredContentWidth) {
85
+ return Math.max(measuredContentWidth + (int) Math.ceil(dpToPx(getContext(), FINAL_WIDTH_PADDING_DP)), 1);
86
+ }
87
+
88
+ private int createExactSpec(Number dimension) {
89
+ return makeMeasureSpec(MeasureSpec.getSize(dpToPx(getContext(), dimension.get())), EXACTLY);
48
90
  }
49
91
 
50
92
  private int resolveActionBarSize() {
@@ -294,6 +294,20 @@ class TitleAndButtonsContainerTest : BaseTest() {
294
294
  assertThat(uut.getTitleComponent().right).isEqualTo(UUT_WIDTH - rightBarWidth - DEFAULT_LEFT_MARGIN_PX)
295
295
  }
296
296
 
297
+ @Test
298
+ fun `Component - title width shrinks by measured right buttons only`() {
299
+ val rightButtonsWidth = 48
300
+ setup(
301
+ rightBarWidth = rightButtonsWidth,
302
+ componentWidth = UUT_WIDTH,
303
+ alignment = Alignment.Default
304
+ )
305
+
306
+ idleMainLooper()
307
+ assertThat(uut.getTitleComponent().left).isEqualTo(DEFAULT_LEFT_MARGIN_PX)
308
+ assertThat(uut.getTitleComponent().right).isEqualTo(UUT_WIDTH - rightButtonsWidth - DEFAULT_LEFT_MARGIN_PX)
309
+ }
310
+
297
311
  @Test
298
312
  fun `Component - should place title between the toolbars`() {
299
313
  val leftBarWidth = 50
@@ -475,4 +489,4 @@ class TitleAndButtonsContainerTest : BaseTest() {
475
489
  }
476
490
 
477
491
  private fun getTitleSubtitleView() = (uut.getTitleComponent() as TitleSubTitleLayout)
478
- }
492
+ }
@@ -0,0 +1,199 @@
1
+ package com.reactnativenavigation.views;
2
+
3
+ import static android.view.View.MeasureSpec.AT_MOST;
4
+ import static android.view.View.MeasureSpec.EXACTLY;
5
+ import static android.view.View.MeasureSpec.getMode;
6
+ import static android.view.View.MeasureSpec.getSize;
7
+ import static android.view.View.MeasureSpec.makeMeasureSpec;
8
+ import static org.assertj.core.api.Java6Assertions.assertThat;
9
+
10
+ import android.app.Activity;
11
+ import android.util.TypedValue;
12
+ import android.view.Gravity;
13
+ import android.view.View;
14
+ import android.view.ViewGroup;
15
+ import android.widget.FrameLayout;
16
+
17
+ import com.reactnativenavigation.BaseTest;
18
+ import com.reactnativenavigation.options.ComponentOptions;
19
+ import com.reactnativenavigation.options.params.Number;
20
+ import com.reactnativenavigation.options.params.Text;
21
+ import com.reactnativenavigation.utils.UiUtils;
22
+ import com.reactnativenavigation.views.stack.topbar.titlebar.TitleBarReactButtonView;
23
+
24
+ import org.junit.Test;
25
+
26
+ import java.util.ArrayList;
27
+ import java.util.List;
28
+
29
+ public class TitleBarReactButtonViewTest extends BaseTest {
30
+ private static final int PARENT_WIDTH = 200;
31
+ private static final int PARENT_HEIGHT = 100;
32
+ private static final int CHILD_WIDTH = 24;
33
+ private static final int CHILD_HEIGHT = 16;
34
+
35
+ @Test
36
+ public void missingDimensionsMeasureToContentThenRemeasureExactForStableAlignment() {
37
+ Activity activity = newActivity();
38
+ TitleBarReactButtonView uut = createView(activity, new ComponentOptions());
39
+ RecordingContentView child = new RecordingContentView(activity);
40
+ setContentView(uut, child);
41
+
42
+ uut.measure(makeMeasureSpec(PARENT_WIDTH, AT_MOST), makeMeasureSpec(PARENT_HEIGHT, AT_MOST));
43
+
44
+ assertThat(uut.getMeasuredWidth()).isEqualTo(finalWidth(activity));
45
+ assertThat(uut.getMeasuredHeight()).isEqualTo(CHILD_HEIGHT);
46
+ assertThat(child.widthMeasureSpecs.size()).isEqualTo(2);
47
+ assertThat(getMode(child.widthMeasureSpecs.get(0))).isEqualTo(AT_MOST);
48
+ assertThat(getSize(child.widthMeasureSpecs.get(0))).isEqualTo(PARENT_WIDTH);
49
+ assertThat(getMode(child.heightMeasureSpecs.get(0))).isEqualTo(AT_MOST);
50
+ assertThat(getSize(child.heightMeasureSpecs.get(0))).isEqualTo(PARENT_HEIGHT);
51
+ assertThat(getMode(child.widthMeasureSpecs.get(1))).isEqualTo(EXACTLY);
52
+ assertThat(getSize(child.widthMeasureSpecs.get(1))).isEqualTo(finalWidth(activity));
53
+ assertThat(getMode(child.heightMeasureSpecs.get(1))).isEqualTo(EXACTLY);
54
+ assertThat(getSize(child.heightMeasureSpecs.get(1))).isEqualTo(CHILD_HEIGHT);
55
+ }
56
+
57
+ @Test
58
+ public void explicitDimensionsMeasureExactly() {
59
+ Activity activity = newActivity();
60
+ ComponentOptions component = new ComponentOptions();
61
+ component.width = new Number(72);
62
+ component.height = new Number(32);
63
+ TitleBarReactButtonView uut = createView(activity, component);
64
+ RecordingContentView child = new RecordingContentView(activity);
65
+ setContentView(uut, child);
66
+
67
+ uut.measure(makeMeasureSpec(PARENT_WIDTH, AT_MOST), makeMeasureSpec(PARENT_HEIGHT, AT_MOST));
68
+
69
+ assertThat(uut.getMeasuredWidth()).isEqualTo(UiUtils.dpToPx(activity, 72));
70
+ assertThat(uut.getMeasuredHeight()).isEqualTo(UiUtils.dpToPx(activity, 32));
71
+ assertThat(child.widthMeasureSpecs.size()).isEqualTo(1);
72
+ assertThat(getMode(child.widthMeasureSpecs.get(0))).isEqualTo(EXACTLY);
73
+ assertThat(getSize(child.widthMeasureSpecs.get(0))).isEqualTo(UiUtils.dpToPx(activity, 72));
74
+ assertThat(getMode(child.heightMeasureSpecs.get(0))).isEqualTo(EXACTLY);
75
+ assertThat(getSize(child.heightMeasureSpecs.get(0))).isEqualTo(UiUtils.dpToPx(activity, 32));
76
+ }
77
+
78
+ @Test
79
+ public void zeroParentSpecsFallbackToBoundedAtMostSpecs() {
80
+ Activity activity = newActivity();
81
+ TitleBarReactButtonView uut = createView(activity, new ComponentOptions());
82
+ RecordingContentView child = new RecordingContentView(activity);
83
+ setContentView(uut, child);
84
+
85
+ uut.measure(makeMeasureSpec(0, AT_MOST), makeMeasureSpec(0, AT_MOST));
86
+
87
+ assertThat(child.widthMeasureSpecs.size()).isEqualTo(2);
88
+ assertThat(getMode(child.widthMeasureSpecs.get(0))).isEqualTo(AT_MOST);
89
+ assertThat(getSize(child.widthMeasureSpecs.get(0)))
90
+ .isEqualTo(Math.max(activity.getResources().getDisplayMetrics().widthPixels, 1));
91
+ assertThat(getMode(child.widthMeasureSpecs.get(1))).isEqualTo(EXACTLY);
92
+ assertThat(getSize(child.widthMeasureSpecs.get(1))).isEqualTo(finalWidth(activity));
93
+ assertThat(getMode(child.heightMeasureSpecs.get(0))).isEqualTo(AT_MOST);
94
+ assertThat(getSize(child.heightMeasureSpecs.get(0))).isEqualTo(Math.max(resolveActionBarSize(activity), 1));
95
+ assertThat(getMode(child.heightMeasureSpecs.get(1))).isEqualTo(EXACTLY);
96
+ assertThat(getSize(child.heightMeasureSpecs.get(1))).isEqualTo(CHILD_HEIGHT);
97
+ }
98
+
99
+ @Test
100
+ public void rtlMissingDimensionsUseBoundedSpecs() {
101
+ Activity activity = newActivity();
102
+ TitleBarReactButtonView uut = createView(activity, new ComponentOptions());
103
+ RecordingContentView child = new RecordingContentView(activity);
104
+ uut.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
105
+ setContentView(uut, child);
106
+
107
+ uut.measure(makeMeasureSpec(PARENT_WIDTH, AT_MOST), makeMeasureSpec(PARENT_HEIGHT, AT_MOST));
108
+
109
+ assertThat(child.widthMeasureSpecs.size()).isEqualTo(2);
110
+ assertThat(getMode(child.widthMeasureSpecs.get(0))).isEqualTo(AT_MOST);
111
+ assertThat(getSize(child.widthMeasureSpecs.get(0))).isEqualTo(PARENT_WIDTH);
112
+ assertThat(getMode(child.widthMeasureSpecs.get(1))).isEqualTo(EXACTLY);
113
+ assertThat(getSize(child.widthMeasureSpecs.get(1))).isEqualTo(finalWidth(activity));
114
+ assertThat(getMode(child.heightMeasureSpecs.get(1))).isEqualTo(EXACTLY);
115
+ assertThat(getSize(child.heightMeasureSpecs.get(1))).isEqualTo(CHILD_HEIGHT);
116
+ }
117
+
118
+ @Test
119
+ public void contentRemainsCenteredWhenMenuCellLaysButtonOutTallerThanMeasuredHeight() {
120
+ Activity activity = newActivity();
121
+ TitleBarReactButtonView uut = createView(activity, new ComponentOptions());
122
+ RecordingContentView child = new RecordingContentView(activity);
123
+ setContentView(uut, child);
124
+
125
+ uut.measure(makeMeasureSpec(PARENT_WIDTH, AT_MOST), makeMeasureSpec(PARENT_HEIGHT, AT_MOST));
126
+ uut.layout(0, 0, uut.getMeasuredWidth(), PARENT_HEIGHT);
127
+
128
+ assertThat(uut.getMeasuredHeight()).isEqualTo(CHILD_HEIGHT);
129
+ assertThat(child.getTop()).isEqualTo((PARENT_HEIGHT - CHILD_HEIGHT) / 2);
130
+ assertThat(child.getBottom()).isEqualTo((PARENT_HEIGHT + CHILD_HEIGHT) / 2);
131
+ }
132
+
133
+ @Test
134
+ public void contentCenteringReplacesExistingVerticalGravityOnly() {
135
+ Activity activity = newActivity();
136
+ TitleBarReactButtonView uut = createView(activity, new ComponentOptions());
137
+ RecordingContentView child = new RecordingContentView(activity);
138
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
139
+ ViewGroup.LayoutParams.MATCH_PARENT,
140
+ ViewGroup.LayoutParams.MATCH_PARENT
141
+ );
142
+ params.gravity = Gravity.TOP | Gravity.RIGHT;
143
+
144
+ uut.addView(child, params);
145
+
146
+ FrameLayout.LayoutParams updatedParams = (FrameLayout.LayoutParams) child.getLayoutParams();
147
+ assertThat(updatedParams.gravity & Gravity.VERTICAL_GRAVITY_MASK).isEqualTo(Gravity.CENTER_VERTICAL);
148
+ assertThat(updatedParams.gravity & Gravity.HORIZONTAL_GRAVITY_MASK).isEqualTo(Gravity.RIGHT);
149
+ }
150
+
151
+ private TitleBarReactButtonView createView(Activity activity, ComponentOptions component) {
152
+ component.name = new Text("ButtonComponent");
153
+ component.componentId = new Text("ButtonComponentId");
154
+ return new TitleBarReactButtonView(activity, component);
155
+ }
156
+
157
+ private void setContentView(TitleBarReactButtonView uut, View child) {
158
+ uut.removeAllViews();
159
+ uut.addView(child, new ViewGroup.LayoutParams(
160
+ ViewGroup.LayoutParams.MATCH_PARENT,
161
+ ViewGroup.LayoutParams.MATCH_PARENT
162
+ ));
163
+ }
164
+
165
+ private int resolveActionBarSize(Activity activity) {
166
+ TypedValue tv = new TypedValue();
167
+ if (activity.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
168
+ return TypedValue.complexToDimensionPixelSize(tv.data, activity.getResources().getDisplayMetrics());
169
+ }
170
+ return UiUtils.dpToPx(activity, 48);
171
+ }
172
+
173
+ private int finalWidth(Activity activity) {
174
+ return CHILD_WIDTH + (int) Math.ceil(UiUtils.dpToPx(activity, 1f));
175
+ }
176
+
177
+ private static class RecordingContentView extends View {
178
+ final List<Integer> widthMeasureSpecs = new ArrayList<>();
179
+ final List<Integer> heightMeasureSpecs = new ArrayList<>();
180
+
181
+ RecordingContentView(Activity activity) {
182
+ super(activity);
183
+ }
184
+
185
+ @Override
186
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
187
+ widthMeasureSpecs.add(widthMeasureSpec);
188
+ heightMeasureSpecs.add(heightMeasureSpec);
189
+
190
+ int measuredWidth = getMode(widthMeasureSpec) == EXACTLY
191
+ ? getSize(widthMeasureSpec)
192
+ : Math.min(CHILD_WIDTH, getSize(widthMeasureSpec));
193
+ int measuredHeight = getMode(heightMeasureSpec) == EXACTLY
194
+ ? getSize(heightMeasureSpec)
195
+ : Math.min(CHILD_HEIGHT, getSize(heightMeasureSpec));
196
+ setMeasuredDimension(measuredWidth, measuredHeight);
197
+ }
198
+ }
199
+ }
@@ -28,6 +28,11 @@ Base class that user's AppDelegate must extend. Handles React Native and navigat
28
28
  - Creates `RCTRootViewFactory` and `ReactHost`
29
29
  - Calls `[ReactNativeNavigation bootstrapWithHost:]` to initialize navigation
30
30
  - Handles RN version differences (0.77, 0.78, 0.79+) via compile-time macros
31
+ - **Deep linking plumbing**:
32
+ - Implements `application:openURL:options:` and `application:continueUserActivity:restorationHandler:`; both call `-dispatchDeepLinkURL:`.
33
+ - `-dispatchDeepLinkURL:` posts `RCTOpenURLNotification` directly if the React runtime is ready, otherwise enqueues the URL.
34
+ - Observes `RCTContentDidAppearNotification` (the Fabric/bridgeless signal) to flush the queue. This solves the cold-start race where URLs (push notifications, OS link launches) arrive before `RCTLinkingManager` is listening.
35
+ - Subclasses can call `-dispatchDeepLinkURL:` manually from notification delegates or any other URL source; the queueing behavior is reused automatically.
31
36
 
32
37
  ### ReactNativeNavigation Bootstrap
33
38
  **File**: `ReactNativeNavigation.h/mm`
@@ -4,6 +4,13 @@
4
4
 
5
5
  @property(nonatomic, strong, readonly) RNNTabBarItemCreator *tabCreator;
6
6
 
7
+ /**
8
+ * When YES, tabs whose options declare `bottomTab.component` skip native
9
+ * icon/text/sfSymbol/role application. The accompanying
10
+ * `RNNCustomTabBarItemView` is responsible for visual rendering of the tab.
11
+ */
12
+ @property(nonatomic, assign) BOOL useCustomItemViews;
13
+
7
14
  - (instancetype)initWithDefaultOptions:(RNNNavigationOptions *)defaultOptions
8
15
  tabCreator:(RNNTabBarItemCreator *)tabCreator;
9
16
 
@@ -36,10 +36,37 @@
36
36
 
37
37
  - (void)createTabBarItem:(UIViewController *)child
38
38
  bottomTabOptions:(RNNBottomTabOptions *)bottomTabOptions {
39
+ if (_useCustomItemViews && bottomTabOptions.component.name.hasValue) {
40
+ UITabBarItem *blankItem = [self createBlankTabBarItem:child
41
+ bottomTabOptions:bottomTabOptions];
42
+ if (blankItem != child.tabBarItem) {
43
+ child.tabBarItem = blankItem;
44
+ }
45
+ return;
46
+ }
47
+
39
48
  UITabBarItem *updatedItem = [_tabCreator createTabBarItem:bottomTabOptions mergeItem:child.tabBarItem];
40
49
  if (updatedItem != child.tabBarItem) {
41
50
  child.tabBarItem = updatedItem;
42
51
  }
43
52
  }
44
53
 
54
+ // Builds a truly blank `UITabBarItem` (nil image, nil title). When custom
55
+ // item views are active, `RNNBottomTabsController` hides the native tab bar
56
+ // visuals and renders the custom row on top. The bar item still needs to
57
+ // exist so that `UITabBarController` reserves the right number of slots and
58
+ // the bottom safe-area inset.
59
+ - (UITabBarItem *)createBlankTabBarItem:(UIViewController *)child
60
+ bottomTabOptions:(RNNBottomTabOptions *)bottomTabOptions {
61
+ UITabBarItem *item = child.tabBarItem ?: [UITabBarItem new];
62
+ item.image = nil;
63
+ item.selectedImage = nil;
64
+ item.title = nil;
65
+ item.tag = bottomTabOptions.tag;
66
+ item.accessibilityIdentifier = [bottomTabOptions.testID withDefault:nil];
67
+ item.accessibilityLabel = [bottomTabOptions.accessibilityLabel withDefault:nil];
68
+ item.imageInsets = UIEdgeInsetsZero;
69
+ return item;
70
+ }
71
+
45
72
  @end
@@ -56,4 +56,20 @@
56
56
  @property(nonatomic) BOOL bridgelessEnabled;
57
57
  #endif
58
58
 
59
+ /**
60
+ * Dispatch a deep link URL through React Native's Linking module so JS
61
+ * subscribers (including RNN's built-in deep linking framework) receive it.
62
+ *
63
+ * Safe to call before the JS bridge is ready: URLs that arrive early
64
+ * (e.g. cold-start notification taps) are queued natively and flushed
65
+ * automatically once Fabric/React content first appears.
66
+ *
67
+ * Custom-scheme and universal-link openings dispatched by the OS are
68
+ * forwarded through this method automatically; call it manually only
69
+ * when your app receives a deep link from a source RNN can't intercept
70
+ * (e.g. a custom `UNUserNotificationCenterDelegate`, a third-party push
71
+ * SDK callback, etc.).
72
+ */
73
+ - (void)dispatchDeepLinkURL:(NSURL *)url;
74
+
59
75
  @end
@@ -8,6 +8,8 @@
8
8
  #import <React/RCTCxxBridgeDelegate.h>
9
9
  #endif
10
10
  #import <React/RCTLegacyViewManagerInteropComponentView.h>
11
+ #import <React/RCTLinkingManager.h>
12
+ #import <React/RCTRootView.h>
11
13
  #import <React/RCTSurfacePresenter.h>
12
14
  #if __has_include(<React/RCTSurfacePresenterStub.h>)
13
15
  #import <React/RCTSurfacePresenterStub.h>
@@ -36,6 +38,13 @@
36
38
 
37
39
  #import <React/RCTComponentViewFactory.h>
38
40
 
41
+ // Deep-link URLs that arrive (openURL, universal link, or external dispatch)
42
+ // before the React runtime is ready are queued here and flushed when Fabric
43
+ // posts `RCTContentDidAppearNotification` — by which point
44
+ // `RCTLinkingManager` is instantiated and JS subscribers are listening.
45
+ static NSMutableArray<NSURL *> *gRNNPendingDeepLinkURLs = nil;
46
+ static BOOL gRNNReactRuntimeReady = NO;
47
+
39
48
 
40
49
  static NSString *const kRNConcurrentRoot = @"concurrentRoot";
41
50
 
@@ -92,9 +101,73 @@ didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
92
101
  [ReactNativeNavigation bootstrapWithHost:self.reactNativeFactory.rootViewFactory.reactHost];
93
102
  #endif
94
103
 
104
+ [self rnn_installDeepLinkObservers];
105
+
95
106
  return YES;
96
107
  }
97
108
 
109
+ #pragma mark - Deep linking
110
+
111
+ // Forward OS-delivered custom-scheme URLs to React Native's Linking module.
112
+ - (BOOL)application:(UIApplication *)application
113
+ openURL:(NSURL *)url
114
+ options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
115
+ [self dispatchDeepLinkURL:url];
116
+ return YES;
117
+ }
118
+
119
+ // Forward universal links (associated domains) to React Native's Linking
120
+ // module by extracting the underlying https URL and routing it through the
121
+ // same pre-bridge queue as everything else.
122
+ - (BOOL)application:(UIApplication *)application
123
+ continueUserActivity:(NSUserActivity *)userActivity
124
+ restorationHandler:
125
+ (void (^)(NSArray<id<UIUserActivityRestoring>> *_Nullable))restorationHandler {
126
+ if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
127
+ [self dispatchDeepLinkURL:userActivity.webpageURL];
128
+ return YES;
129
+ }
130
+ return NO;
131
+ }
132
+
133
+ - (void)dispatchDeepLinkURL:(NSURL *)url {
134
+ if (url == nil) {
135
+ return;
136
+ }
137
+ if (gRNNReactRuntimeReady) {
138
+ [RCTLinkingManager application:[UIApplication sharedApplication]
139
+ openURL:url
140
+ options:@{}];
141
+ return;
142
+ }
143
+ if (gRNNPendingDeepLinkURLs == nil) {
144
+ gRNNPendingDeepLinkURLs = [NSMutableArray array];
145
+ }
146
+ [gRNNPendingDeepLinkURLs addObject:url];
147
+ }
148
+
149
+ - (void)rnn_installDeepLinkObservers {
150
+ // `RCTContentDidAppearNotification` is posted by Fabric's root view
151
+ // once content has rendered. RNN forces bridgeless/new-arch, so the
152
+ // legacy `RCTJavaScriptDidLoadNotification` never fires; we rely on
153
+ // this Fabric signal exclusively.
154
+ [[NSNotificationCenter defaultCenter] addObserver:self
155
+ selector:@selector(rnn_handleReactRuntimeReady:)
156
+ name:RCTContentDidAppearNotification
157
+ object:nil];
158
+ }
159
+
160
+ - (void)rnn_handleReactRuntimeReady:(NSNotification *)notification {
161
+ gRNNReactRuntimeReady = YES;
162
+ NSArray<NSURL *> *pending = [gRNNPendingDeepLinkURLs copy];
163
+ [gRNNPendingDeepLinkURLs removeAllObjects];
164
+ for (NSURL *url in pending) {
165
+ [RCTLinkingManager application:[UIApplication sharedApplication]
166
+ openURL:url
167
+ options:@{}];
168
+ }
169
+ }
170
+
98
171
 
99
172
  #if !RNN_RN_VERSION_79_OR_NEWER
100
173
  - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
@@ -1,3 +1,4 @@
1
+ #import "RNNComponentOptions.h"
1
2
  #import "RNNOptions.h"
2
3
 
3
4
  @class DotIndicatorOptions;
@@ -5,6 +6,7 @@
5
6
  @interface RNNBottomTabOptions : RNNOptions
6
7
 
7
8
  @property(nonatomic) NSUInteger tag;
9
+ @property(nonatomic, strong) RNNComponentOptions *component;
8
10
  @property(nonatomic, strong) Text *text;
9
11
  @property(nonatomic, strong) Text *badge;
10
12
  @property(nonatomic, strong) Color *badgeColor;
@@ -8,6 +8,9 @@
8
8
  self = [super initWithDict:dict];
9
9
  self.tag = arc4random();
10
10
 
11
+ self.component =
12
+ [[RNNComponentOptions alloc] initWithDict:[dict objectForKey:@"component"]];
13
+
11
14
  self.text = [TextParser parse:dict key:@"text"];
12
15
  self.badge = [TextParser parse:dict key:@"badge"];
13
16
  self.fontFamily = [TextParser parse:dict key:@"fontFamily"];
@@ -38,6 +41,7 @@
38
41
 
39
42
  - (void)mergeOptions:(RNNBottomTabOptions *)options {
40
43
  [self.dotIndicator mergeOptions:options.dotIndicator];
44
+ [self.component mergeOptions:options.component];
41
45
 
42
46
  if (options.text.hasValue)
43
47
  self.text = options.text;
@@ -88,7 +92,7 @@
88
92
  self.iconColor.hasValue || self.selectedIconColor.hasValue ||
89
93
  self.selectedTextColor.hasValue || self.iconInsets.hasValue || self.textColor.hasValue ||
90
94
  self.visible.hasValue || self.selectTabOnPress.hasValue || self.sfSymbol.hasValue ||
91
- self.sfSelectedSymbol.hasValue || self.role.hasValue;
95
+ self.sfSelectedSymbol.hasValue || self.role.hasValue || self.component.hasValue;
92
96
  }
93
97
 
94
98
  @end
@@ -3,6 +3,7 @@
3
3
  #import "RNNBottomTabsPresenter.h"
4
4
  #import "RNNDotIndicatorPresenter.h"
5
5
  #import "RNNEventEmitter.h"
6
+ #import "RNNReactComponentRegistry.h"
6
7
  #import "UIViewController+LayoutProtocol.h"
7
8
  #import <UIKit/UIKit.h>
8
9
 
@@ -16,6 +17,7 @@
16
17
  presenter:(RNNBasePresenter *)presenter
17
18
  bottomTabPresenter:(BottomTabPresenter *)bottomTabPresenter
18
19
  dotIndicatorPresenter:(RNNDotIndicatorPresenter *)dotIndicatorPresenter
20
+ componentRegistry:(RNNReactComponentRegistry *)componentRegistry
19
21
  eventEmitter:(RNNEventEmitter *)eventEmitter
20
22
  childViewControllers:(NSArray *)childViewControllers
21
23
  bottomTabsAttacher:(BottomTabsBaseAttacher *)bottomTabsAttacher;