@lodev09/react-native-true-sheet 3.7.3 → 3.7.4-beta.1

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 (27) hide show
  1. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +0 -4
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +29 -30
  3. package/android/src/main/java/com/lodev09/truesheet/core/RNScreensFragmentObserver.kt +65 -14
  4. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +1 -1
  5. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h +4 -0
  6. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +13 -0
  7. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.h +11 -0
  8. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.cpp +12 -0
  9. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.h +10 -0
  10. package/ios/TrueSheetContainerView.mm +7 -15
  11. package/ios/TrueSheetContentView.h +2 -2
  12. package/ios/TrueSheetContentView.mm +84 -89
  13. package/ios/TrueSheetHeaderView.mm +1 -3
  14. package/ios/TrueSheetView.mm +72 -14
  15. package/ios/TrueSheetViewController.h +0 -15
  16. package/ios/TrueSheetViewController.mm +76 -146
  17. package/ios/core/RNScreensEventObserver.h +43 -0
  18. package/ios/core/RNScreensEventObserver.mm +119 -0
  19. package/ios/core/TrueSheetBlurView.mm +26 -22
  20. package/ios/utils/LayoutUtil.h +23 -0
  21. package/ios/utils/LayoutUtil.mm +28 -3
  22. package/lib/module/TrueSheet.js +16 -5
  23. package/lib/module/TrueSheet.js.map +1 -1
  24. package/lib/typescript/src/TrueSheet.d.ts +4 -0
  25. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  26. package/package.json +1 -1
  27. package/src/TrueSheet.tsx +16 -3
@@ -483,10 +483,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
483
483
  eventDispatcher?.dispatchEvent(BackPressEvent(surfaceId, id))
484
484
  }
485
485
 
486
- override fun viewControllerDidDetectScreenDisappear() {
487
- dismissAll(animated = true) {}
488
- }
489
-
490
486
  override fun viewControllerDidDetectScreenDismiss() {
491
487
  resetTranslation()
492
488
  }
@@ -63,7 +63,6 @@ interface TrueSheetViewControllerDelegate {
63
63
  fun viewControllerWillBlur()
64
64
  fun viewControllerDidBlur()
65
65
  fun viewControllerDidBackPress()
66
- fun viewControllerDidDetectScreenDisappear()
67
66
  fun viewControllerDidDetectScreenDismiss()
68
67
  }
69
68
 
@@ -92,7 +91,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
92
91
  private const val DEFAULT_CORNER_RADIUS = 16 // dp
93
92
  private const val TRANSLATE_ANIMATION_DURATION = 200L
94
93
  private const val DISMISS_DURATION = 200L
95
- private const val MODAL_FADE_DURATION = 150L
94
+ private const val SCREEN_FADE_DURATION = 150L
96
95
  }
97
96
 
98
97
  // =============================================================================
@@ -132,7 +131,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
132
131
 
133
132
  private var interactionState: InteractionState = InteractionState.Idle
134
133
  private var isDismissing = false
135
- internal var wasHiddenByModal = false
134
+ internal var wasHiddenByScreen = false
136
135
  private var shouldAnimatePresent = false
137
136
  private var isPresentAnimating = false
138
137
 
@@ -340,7 +339,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
340
339
  isDismissing = false
341
340
  isPresented = false
342
341
  isSheetVisible = false
343
- wasHiddenByModal = false
342
+ wasHiddenByScreen = false
344
343
  cachedContentHeight = 0
345
344
  cachedHeaderHeight = 0
346
345
  isPresentAnimating = false
@@ -568,32 +567,32 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
568
567
  private fun setupModalObserver() {
569
568
  rnScreensObserver = RNScreensFragmentObserver(
570
569
  reactContext = reactContext,
571
- onModalPresented = {
572
- if (isPresented && isSheetVisible && isTopmostSheet) {
573
- dismissKeyboard()
574
- post { hideForModal() }
570
+ onScreenPresented = {
571
+ if (isPresented && isTopmostSheet) {
572
+ if (isSheetVisible) {
573
+ dismissKeyboard()
574
+ post { hideForScreen() }
575
+ } else {
576
+ // Sheet is already hidden, just mark it
577
+ wasHiddenByScreen = true
578
+ }
575
579
  }
576
580
  },
577
- onModalWillDismiss = {
578
- if (isPresented && wasHiddenByModal && isTopmostSheet) {
579
- showAfterModal()
581
+ onScreenWillDismiss = {
582
+ val hasPushedScreens = rnScreensObserver?.hasPushedScreens == true
583
+ if (isPresented && wasHiddenByScreen && isTopmostSheet && !hasPushedScreens) {
584
+ showAfterScreen()
580
585
  delegate?.viewControllerDidDetectScreenDismiss()
581
586
  }
582
587
  },
583
- onModalDidDismiss = {
584
- if (isPresented && wasHiddenByModal) {
585
- wasHiddenByModal = false
588
+ onScreenDidDismiss = {
589
+ if (isPresented && wasHiddenByScreen) {
590
+ wasHiddenByScreen = false
586
591
  // Restore parent sheet after this sheet is restored
587
592
  parentSheetView?.viewController?.let { parent ->
588
- post { parent.showAfterModal() }
593
+ post { parent.showAfterScreen() }
589
594
  }
590
595
  }
591
- },
592
- onNonModalScreenPushed = {
593
- // Only handle on root sheet (no parent) to trigger dismissAll
594
- if (isPresented && isSheetVisible && parentSheetView == null) {
595
- delegate?.viewControllerDidDetectScreenDisappear()
596
- }
597
596
  }
598
597
  )
599
598
  rnScreensObserver?.start()
@@ -609,29 +608,29 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
609
608
  dimViews.forEach { it.visibility = if (visible) VISIBLE else INVISIBLE }
610
609
  }
611
610
 
612
- private fun hideForModal() {
611
+ private fun hideForScreen() {
613
612
  val sheet = sheetView ?: run {
614
- RNLog.e(reactContext, "TrueSheet: sheetView is null in hideForModal")
613
+ RNLog.e(reactContext, "TrueSheet: sheetView is null in hideForScreen")
615
614
  return
616
615
  }
617
616
 
618
617
  isSheetVisible = false
619
- wasHiddenByModal = true
618
+ wasHiddenByScreen = true
620
619
 
621
- dimViews.forEach { it.animate().alpha(0f).setDuration(MODAL_FADE_DURATION).start() }
620
+ dimViews.forEach { it.animate().alpha(0f).setDuration(SCREEN_FADE_DURATION).start() }
622
621
  sheet.animate()
623
622
  .alpha(0f)
624
- .setDuration(MODAL_FADE_DURATION)
623
+ .setDuration(SCREEN_FADE_DURATION)
625
624
  .withEndAction {
626
625
  setSheetVisibility(false)
627
626
  }
628
627
  .start()
629
628
 
630
629
  // This will hide parent sheets first
631
- parentSheetView?.viewController?.hideForModal()
630
+ parentSheetView?.viewController?.hideForScreen()
632
631
  }
633
632
 
634
- private fun showAfterModal() {
633
+ private fun showAfterScreen() {
635
634
  isSheetVisible = true
636
635
  setSheetVisibility(true)
637
636
  sheetView?.alpha = 1f
@@ -643,7 +642,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
643
642
  * Android may restore visibility on activity resume, so we need to hide it again.
644
643
  */
645
644
  fun reapplyHiddenState() {
646
- if (!wasHiddenByModal) return
645
+ if (!wasHiddenByScreen) return
647
646
  setSheetVisibility(false)
648
647
  }
649
648
 
@@ -954,7 +953,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
954
953
  // =============================================================================
955
954
 
956
955
  private fun shouldHandleKeyboard(checkFocus: Boolean = true): Boolean {
957
- if (wasHiddenByModal) return false
956
+ if (wasHiddenByScreen) return false
958
957
  if (!isTopmostSheet) return false
959
958
  if (checkFocus && !isFocusedViewWithinSheet()) return false
960
959
  return true
@@ -7,27 +7,30 @@ import androidx.fragment.app.FragmentManager
7
7
  import androidx.lifecycle.DefaultLifecycleObserver
8
8
  import androidx.lifecycle.LifecycleOwner
9
9
  import com.facebook.react.bridge.ReactContext
10
-
11
10
  private const val RN_SCREENS_PACKAGE = "com.swmansion.rnscreens"
12
11
 
13
12
  /**
14
- * Observes fragment lifecycle to detect react-native-screens modal presentation.
15
- * Automatically notifies when modals are presented/dismissed.
13
+ * Observes fragment lifecycle to detect react-native-screens screen presentation.
14
+ * Automatically notifies when screens (modals or pushed) are presented/dismissed.
16
15
  */
17
16
  class RNScreensFragmentObserver(
18
17
  private val reactContext: ReactContext,
19
- private val onModalPresented: () -> Unit,
20
- private val onModalWillDismiss: () -> Unit,
21
- private val onModalDidDismiss: () -> Unit,
22
- private val onNonModalScreenPushed: () -> Unit
18
+ private val onScreenPresented: () -> Unit,
19
+ private val onScreenWillDismiss: () -> Unit,
20
+ private val onScreenDidDismiss: () -> Unit
23
21
  ) {
24
22
  private var fragmentLifecycleCallback: FragmentManager.FragmentLifecycleCallbacks? = null
25
23
  private var activityLifecycleObserver: DefaultLifecycleObserver? = null
26
24
  private val activeModalFragments: MutableSet<Fragment> = mutableSetOf()
25
+ private val activePushedFragments: MutableSet<Fragment> = mutableSetOf()
27
26
  private var isActivityInForeground = true
28
27
  private var pendingDismissRunnable: Runnable? = null
28
+ private var pendingPopRunnable: Runnable? = null
29
29
  private var isInitialized = false
30
30
 
31
+ val hasPushedScreens: Boolean
32
+ get() = activePushedFragments.isNotEmpty()
33
+
31
34
  /**
32
35
  * Start observing fragment lifecycle events.
33
36
  */
@@ -64,11 +67,22 @@ class RNScreensFragmentObserver(
64
67
  activeModalFragments.add(f)
65
68
 
66
69
  if (activeModalFragments.size == 1) {
67
- onModalPresented()
70
+ onScreenPresented()
71
+ }
72
+ } else if (activeModalFragments.isEmpty() && pendingDismissRunnable == null && isNonModalScreenFragment(f)) {
73
+ // Check if this is a new push or just re-attaching during pop
74
+ val isPendingPop = pendingPopRunnable != null
75
+
76
+ if (isPendingPop) {
77
+ // Screen is being re-attached during pop transition, ignore it
78
+ return
79
+ }
80
+
81
+ activePushedFragments.add(f)
82
+
83
+ if (activePushedFragments.size == 1) {
84
+ onScreenPresented()
68
85
  }
69
- } else if (activeModalFragments.isEmpty() && isNonModalScreenFragment(f)) {
70
- // Only trigger non-modal push when no modals are active
71
- onNonModalScreenPushed()
72
86
  }
73
87
  }
74
88
 
@@ -86,14 +100,24 @@ class RNScreensFragmentObserver(
86
100
  // Post dismiss to allow fragment attach to cancel if navigation is happening
87
101
  schedulePendingDismiss()
88
102
  }
103
+ } else if (activePushedFragments.contains(f) && f.isRemoving) {
104
+ activePushedFragments.remove(f)
105
+
106
+ if (activePushedFragments.isEmpty()) {
107
+ schedulePendingPop()
108
+ }
89
109
  }
90
110
  }
91
111
 
92
112
  override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) {
93
113
  super.onFragmentDestroyed(fm, f)
94
114
 
95
- if (activeModalFragments.isEmpty() && pendingDismissRunnable == null) {
96
- onModalDidDismiss()
115
+ if (activeModalFragments.isEmpty() &&
116
+ activePushedFragments.isEmpty() &&
117
+ pendingDismissRunnable == null &&
118
+ pendingPopRunnable == null
119
+ ) {
120
+ onScreenDidDismiss()
97
121
  }
98
122
  }
99
123
  }
@@ -113,6 +137,7 @@ class RNScreensFragmentObserver(
113
137
  val activity = reactContext.currentActivity as? AppCompatActivity
114
138
 
115
139
  cancelPendingDismiss()
140
+ cancelPendingPop()
116
141
 
117
142
  fragmentLifecycleCallback?.let { callback ->
118
143
  activity?.supportFragmentManager?.unregisterFragmentLifecycleCallbacks(callback)
@@ -125,6 +150,7 @@ class RNScreensFragmentObserver(
125
150
  activityLifecycleObserver = null
126
151
 
127
152
  activeModalFragments.clear()
153
+ activePushedFragments.clear()
128
154
  }
129
155
 
130
156
  private fun schedulePendingDismiss() {
@@ -136,7 +162,7 @@ class RNScreensFragmentObserver(
136
162
  pendingDismissRunnable = Runnable {
137
163
  pendingDismissRunnable = null
138
164
  if (activeModalFragments.isEmpty()) {
139
- onModalWillDismiss()
165
+ onScreenWillDismiss()
140
166
  }
141
167
  }
142
168
  decorView.post(pendingDismissRunnable)
@@ -152,6 +178,31 @@ class RNScreensFragmentObserver(
152
178
  }
153
179
  }
154
180
 
181
+ private fun schedulePendingPop() {
182
+ val activity = reactContext.currentActivity ?: return
183
+ val decorView = activity.window?.decorView ?: return
184
+
185
+ cancelPendingPop()
186
+
187
+ pendingPopRunnable = Runnable {
188
+ pendingPopRunnable = null
189
+ if (activePushedFragments.isEmpty()) {
190
+ onScreenWillDismiss()
191
+ }
192
+ }
193
+ decorView.post(pendingPopRunnable)
194
+ }
195
+
196
+ private fun cancelPendingPop() {
197
+ val activity = reactContext.currentActivity ?: return
198
+ val decorView = activity.window?.decorView ?: return
199
+
200
+ pendingPopRunnable?.let {
201
+ decorView.removeCallbacks(it)
202
+ pendingPopRunnable = null
203
+ }
204
+ }
205
+
155
206
  companion object {
156
207
  /**
157
208
  * Check if fragment is from react-native-screens.
@@ -166,7 +166,7 @@ object TrueSheetStackManager {
166
166
  while (true) {
167
167
  val parent = current.viewController.parentSheetView ?: return current
168
168
 
169
- if (parent.viewController.wasHiddenByModal) {
169
+ if (parent.viewController.wasHiddenByScreen) {
170
170
  return current
171
171
  }
172
172
 
@@ -18,6 +18,10 @@ class TrueSheetViewComponentDescriptor final
18
18
  concreteShadowNode.adjustLayoutWithState();
19
19
 
20
20
  ConcreteComponentDescriptor::adopt(shadowNode);
21
+
22
+ #if !defined(ANDROID)
23
+ concreteShadowNode.setEventDispatcher(eventDispatcher_);
24
+ #endif
21
25
  }
22
26
  };
23
27
 
@@ -45,4 +45,17 @@ void TrueSheetViewShadowNode::adjustLayoutWithState() {
45
45
  }
46
46
  }
47
47
 
48
+ #if !defined(ANDROID)
49
+ void TrueSheetViewShadowNode::setEventDispatcher(
50
+ std::weak_ptr<const EventDispatcher> dispatcher) {
51
+ getStateDataMutable().setEventDispatcher(dispatcher);
52
+ }
53
+
54
+ TrueSheetViewShadowNode::StateData &
55
+ TrueSheetViewShadowNode::getStateDataMutable() {
56
+ ensureUnsealed();
57
+ return const_cast<TrueSheetViewShadowNode::StateData &>(getStateData());
58
+ }
59
+ #endif
60
+
48
61
  } // namespace facebook::react
@@ -8,6 +8,8 @@
8
8
 
9
9
  namespace facebook::react {
10
10
 
11
+ class EventDispatcher;
12
+
11
13
  JSI_EXPORT extern const char TrueSheetViewComponentName[];
12
14
 
13
15
  /*
@@ -22,6 +24,8 @@ class JSI_EXPORT TrueSheetViewShadowNode final
22
24
  using ConcreteViewShadowNode::ConcreteViewShadowNode;
23
25
 
24
26
  public:
27
+ using StateData = ConcreteViewShadowNode::ConcreteStateData;
28
+
25
29
  static ShadowNodeTraits BaseTraits() {
26
30
  auto traits = ConcreteViewShadowNode::BaseTraits();
27
31
  traits.set(ShadowNodeTraits::Trait::RootNodeKind);
@@ -29,6 +33,13 @@ class JSI_EXPORT TrueSheetViewShadowNode final
29
33
  }
30
34
 
31
35
  void adjustLayoutWithState();
36
+
37
+ #if !defined(ANDROID)
38
+ void setEventDispatcher(std::weak_ptr<const EventDispatcher> dispatcher);
39
+
40
+ private:
41
+ StateData &getStateDataMutable();
42
+ #endif
32
43
  };
33
44
 
34
45
  } // namespace facebook::react
@@ -8,4 +8,16 @@ folly::dynamic TrueSheetViewState::getDynamic() const {
8
8
  }
9
9
  #endif
10
10
 
11
+ #if !defined(ANDROID)
12
+ void TrueSheetViewState::setEventDispatcher(
13
+ std::weak_ptr<const EventDispatcher> dispatcher) {
14
+ eventDispatcher_ = dispatcher;
15
+ }
16
+
17
+ std::weak_ptr<const EventDispatcher> TrueSheetViewState::getEventDispatcher()
18
+ const noexcept {
19
+ return eventDispatcher_;
20
+ }
21
+ #endif
22
+
11
23
  } // namespace facebook::react
@@ -10,6 +10,8 @@
10
10
 
11
11
  namespace facebook::react {
12
12
 
13
+ class EventDispatcher;
14
+
13
15
  /*
14
16
  * State for <TrueSheetView> component.
15
17
  * Contains the container dimensions from native.
@@ -37,6 +39,14 @@ class TrueSheetViewState final {
37
39
  return MapBufferBuilder::EMPTY();
38
40
  }
39
41
  #endif
42
+
43
+ #if !defined(ANDROID)
44
+ void setEventDispatcher(std::weak_ptr<const EventDispatcher> dispatcher);
45
+ std::weak_ptr<const EventDispatcher> getEventDispatcher() const noexcept;
46
+
47
+ private:
48
+ std::weak_ptr<const EventDispatcher> eventDispatcher_;
49
+ #endif
40
50
  };
41
51
 
42
52
  } // namespace facebook::react
@@ -79,7 +79,7 @@ using namespace facebook::react;
79
79
 
80
80
  - (void)setupContentScrollViewPinning {
81
81
  if (_scrollViewPinningSet && _contentView) {
82
- [_contentView setupScrollViewPinning:_scrollViewPinningEnabled withHeaderView:_headerView];
82
+ [_contentView setupScrollViewPinning:_scrollViewPinningEnabled];
83
83
  }
84
84
  }
85
85
 
@@ -104,10 +104,6 @@ using namespace facebook::react;
104
104
  }
105
105
  _headerView = (TrueSheetHeaderView *)childComponentView;
106
106
  _headerView.delegate = self;
107
-
108
- if (_contentView) {
109
- [self setupContentScrollViewPinning];
110
- }
111
107
  [self headerViewDidChangeSize:_headerView.frame.size];
112
108
  }
113
109
 
@@ -129,10 +125,6 @@ using namespace facebook::react;
129
125
  if ([childComponentView isKindOfClass:[TrueSheetHeaderView class]]) {
130
126
  _headerView.delegate = nil;
131
127
  _headerView = nil;
132
-
133
- if (_contentView) {
134
- [self setupContentScrollViewPinning];
135
- }
136
128
  [self headerViewDidChangeSize:CGSizeZero];
137
129
  }
138
130
 
@@ -150,21 +142,21 @@ using namespace facebook::react;
150
142
  #pragma mark - TrueSheetContentViewDelegate
151
143
 
152
144
  - (void)contentViewDidChangeSize:(CGSize)newSize {
153
- if ([self.delegate respondsToSelector:@selector(containerViewContentDidChangeSize:)]) {
154
- [self.delegate containerViewContentDidChangeSize:newSize];
155
- }
145
+ [self.delegate containerViewContentDidChangeSize:newSize];
156
146
  }
157
147
 
158
148
  - (void)contentViewDidChangeChildren {
159
149
  [self setupContentScrollViewPinning];
160
150
  }
161
151
 
152
+ - (void)contentViewDidChangeInsets {
153
+ [self setupContentScrollViewPinning];
154
+ }
155
+
162
156
  #pragma mark - TrueSheetHeaderViewDelegate
163
157
 
164
158
  - (void)headerViewDidChangeSize:(CGSize)newSize {
165
- if ([self.delegate respondsToSelector:@selector(containerViewHeaderDidChangeSize:)]) {
166
- [self.delegate containerViewHeaderDidChangeSize:newSize];
167
- }
159
+ [self.delegate containerViewHeaderDidChangeSize:newSize];
168
160
  }
169
161
 
170
162
  #pragma mark - Keyboard Handling
@@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN
22
22
 
23
23
  - (void)contentViewDidChangeSize:(CGSize)newSize;
24
24
  - (void)contentViewDidChangeChildren;
25
+ - (void)contentViewDidChangeInsets;
25
26
 
26
27
  @end
27
28
 
@@ -34,9 +35,8 @@ NS_ASSUME_NONNULL_BEGIN
34
35
  /**
35
36
  * Setup ScrollView pinning
36
37
  * @param pinned Whether to pin the scroll view
37
- * @param headerView Optional header view to pin below (can be nil)
38
38
  */
39
- - (void)setupScrollViewPinning:(BOOL)pinned withHeaderView:(nullable UIView *)headerView;
39
+ - (void)setupScrollViewPinning:(BOOL)pinned;
40
40
 
41
41
  @end
42
42