@lodev09/react-native-true-sheet 3.7.4-beta.2 → 3.8.0-beta.0

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 (34) hide show
  1. package/README.md +9 -48
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +24 -0
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +162 -1
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +2 -3
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +76 -8
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +70 -74
  7. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +5 -0
  8. package/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +63 -0
  9. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +1 -0
  10. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +2 -19
  11. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetKeyboardObserver.kt +34 -6
  12. package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +12 -0
  13. package/ios/TrueSheetContainerView.h +15 -4
  14. package/ios/TrueSheetContainerView.mm +45 -6
  15. package/ios/TrueSheetContentView.h +6 -2
  16. package/ios/TrueSheetContentView.mm +88 -2
  17. package/ios/TrueSheetFooterView.h +4 -3
  18. package/ios/TrueSheetFooterView.mm +16 -63
  19. package/ios/TrueSheetView.mm +31 -7
  20. package/ios/core/TrueSheetKeyboardObserver.h +38 -0
  21. package/ios/core/TrueSheetKeyboardObserver.mm +90 -0
  22. package/lib/module/TrueSheet.js +3 -1
  23. package/lib/module/TrueSheet.js.map +1 -1
  24. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +5 -0
  25. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  26. package/lib/typescript/src/TrueSheet.types.d.ts +15 -0
  27. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  28. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +4 -0
  29. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  30. package/package.json +1 -1
  31. package/src/TrueSheet.tsx +3 -1
  32. package/src/TrueSheet.types.ts +17 -0
  33. package/src/fabric/TrueSheetViewNativeComponent.ts +5 -0
  34. package/android/src/main/java/com/lodev09/truesheet/core/RNScreensFragmentObserver.kt +0 -266
package/README.md CHANGED
@@ -29,9 +29,16 @@ The true native bottom sheet experience for your React Native Apps. 💩
29
29
 
30
30
  ### Prerequisites
31
31
 
32
- - React Native >= 0.76 (Expo SDK 52+)
32
+ - React Native 0.80+
33
33
  - New Architecture enabled (default in RN 0.76+)
34
- - Xcode 26.2 (strongly recommended for better library functionality)
34
+ - Xcode 26.1+ (liquid glass support)
35
+
36
+ ### Compatibility
37
+
38
+ | TrueSheet | React Native | Expo SDK |
39
+ |-----------|--------------|----------|
40
+ | 3.7+ | 0.80+ | 54+ |
41
+ | 3.6 | 0.79 | 52-53 |
35
42
 
36
43
  ### Expo
37
44
 
@@ -46,27 +53,6 @@ yarn add @lodev09/react-native-true-sheet
46
53
  cd ios && pod install
47
54
  ```
48
55
 
49
- ### EAS Build (iOS)
50
-
51
- When using [EAS Build](https://docs.expo.dev/build/introduction/) to build your iOS app, you must configure your `eas.json` to use a build image that includes Xcode 26.2. Use `"image": "latest"` or choose from the [available build images](https://docs.expo.dev/build-reference/infrastructure/#ios-server-images):
52
-
53
- ```json
54
- {
55
- "build": {
56
- "production": {
57
- "ios": {
58
- "image": "latest"
59
- }
60
- },
61
- "development": {
62
- "ios": {
63
- "image": "latest"
64
- }
65
- }
66
- }
67
- }
68
- ```
69
-
70
56
  ## Documentation
71
57
 
72
58
  - [Example](example)
@@ -111,31 +97,6 @@ export const App = () => {
111
97
  }
112
98
  ```
113
99
 
114
- ## Testing
115
-
116
- TrueSheet exports mocks for easy testing:
117
-
118
- ```tsx
119
- // Main component
120
- jest.mock('@lodev09/react-native-true-sheet', () =>
121
- require('@lodev09/react-native-true-sheet/mock')
122
- );
123
-
124
- // Navigation (if using)
125
- jest.mock('@lodev09/react-native-true-sheet/navigation', () =>
126
- require('@lodev09/react-native-true-sheet/navigation/mock')
127
- );
128
-
129
- // Reanimated (if using)
130
- jest.mock('@lodev09/react-native-true-sheet/reanimated', () =>
131
- require('@lodev09/react-native-true-sheet/reanimated/mock')
132
- );
133
- ```
134
-
135
- All methods (`present`, `dismiss`, `resize`) are mocked as Jest functions, allowing you to test your components without native dependencies.
136
-
137
- **[Full Testing Guide](https://sheet.lodev09.com/guides/jest)**
138
-
139
100
  ## Contributing
140
101
 
141
102
  See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
@@ -2,6 +2,7 @@ package com.lodev09.truesheet
2
2
 
3
3
  import android.annotation.SuppressLint
4
4
  import android.view.View
5
+ import com.facebook.react.bridge.ReadableMap
5
6
  import com.facebook.react.uimanager.ThemedReactContext
6
7
  import com.facebook.react.uimanager.events.EventDispatcher
7
8
  import com.facebook.react.views.view.ReactViewGroup
@@ -34,6 +35,15 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
34
35
  var headerHeight: Int = 0
35
36
  var footerHeight: Int = 0
36
37
 
38
+ var insetAdjustment: String = "automatic"
39
+ var scrollViewBottomInset: Int = 0
40
+ var scrollViewPinningEnabled: Boolean = false
41
+ var scrollableOptions: ReadableMap? = null
42
+ set(value) {
43
+ field = value
44
+ contentView?.scrollableOptions = value
45
+ }
46
+
37
47
  override val eventDispatcher: EventDispatcher?
38
48
  get() = delegate?.eventDispatcher
39
49
 
@@ -43,12 +53,26 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
43
53
  clipToPadding = false
44
54
  }
45
55
 
56
+ fun setupContentScrollViewPinning() {
57
+ val bottomInset = if (insetAdjustment == "automatic") scrollViewBottomInset else 0
58
+ contentView?.setupScrollViewPinning(scrollViewPinningEnabled, bottomInset)
59
+ }
60
+
61
+ fun setupKeyboardHandler() {
62
+ contentView?.setupKeyboardHandler()
63
+ }
64
+
65
+ fun cleanupKeyboardHandler() {
66
+ contentView?.cleanupKeyboardHandler()
67
+ }
68
+
46
69
  override fun addView(child: View?, index: Int) {
47
70
  super.addView(child, index)
48
71
 
49
72
  when (child) {
50
73
  is TrueSheetContentView -> {
51
74
  child.delegate = this
75
+ child.scrollableOptions = scrollableOptions
52
76
  contentView = child
53
77
  }
54
78
 
@@ -1,8 +1,15 @@
1
1
  package com.lodev09.truesheet
2
2
 
3
3
  import android.annotation.SuppressLint
4
+ import android.view.View
5
+ import android.view.ViewGroup
6
+ import android.widget.ScrollView
7
+ import com.facebook.react.bridge.ReadableMap
8
+ import com.facebook.react.uimanager.PixelUtil.dpToPx
4
9
  import com.facebook.react.uimanager.ThemedReactContext
5
10
  import com.facebook.react.views.view.ReactViewGroup
11
+ import com.lodev09.truesheet.core.TrueSheetKeyboardObserver
12
+ import com.lodev09.truesheet.core.TrueSheetKeyboardObserverDelegate
6
13
 
7
14
  /**
8
15
  * Delegate interface for content view size changes
@@ -16,12 +23,25 @@ interface TrueSheetContentViewDelegate {
16
23
  * This is the first child of TrueSheetContainerView
17
24
  */
18
25
  @SuppressLint("ViewConstructor")
19
- class TrueSheetContentView(context: ThemedReactContext) : ReactViewGroup(context) {
26
+ class TrueSheetContentView(private val reactContext: ThemedReactContext) : ReactViewGroup(reactContext) {
20
27
  var delegate: TrueSheetContentViewDelegate? = null
21
28
 
22
29
  private var lastWidth = 0
23
30
  private var lastHeight = 0
24
31
 
32
+ private var pinnedScrollView: ScrollView? = null
33
+ private var originalScrollViewPaddingBottom: Int = 0
34
+ private var bottomInset: Int = 0
35
+
36
+ private var keyboardScrollOffset: Float = 0f
37
+ private var keyboardObserver: TrueSheetKeyboardObserver? = null
38
+
39
+ var scrollableOptions: ReadableMap? = null
40
+ set(value) {
41
+ field = value
42
+ keyboardScrollOffset = value?.getDouble("keyboardScrollOffset")?.toFloat()?.dpToPx() ?: 0f
43
+ }
44
+
25
45
  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
26
46
  super.onSizeChanged(w, h, oldw, oldh)
27
47
 
@@ -32,6 +52,147 @@ class TrueSheetContentView(context: ThemedReactContext) : ReactViewGroup(context
32
52
  }
33
53
  }
34
54
 
55
+ fun setupScrollViewPinning(enabled: Boolean, bottomInset: Int) {
56
+ if (!enabled) {
57
+ clearScrollViewPinning()
58
+ return
59
+ }
60
+
61
+ this.bottomInset = bottomInset
62
+ applyScrollViewBottomInset()
63
+ }
64
+
65
+ private fun applyScrollViewBottomInset() {
66
+ val scrollView = findScrollView(this)
67
+
68
+ if (scrollView != pinnedScrollView) {
69
+ // Restore previous scroll view's padding
70
+ pinnedScrollView?.setPadding(
71
+ pinnedScrollView!!.paddingLeft,
72
+ pinnedScrollView!!.paddingTop,
73
+ pinnedScrollView!!.paddingRight,
74
+ originalScrollViewPaddingBottom
75
+ )
76
+
77
+ pinnedScrollView = scrollView
78
+ originalScrollViewPaddingBottom = scrollView?.paddingBottom ?: 0
79
+ }
80
+
81
+ scrollView?.let {
82
+ it.clipToPadding = false
83
+ it.setPadding(
84
+ it.paddingLeft,
85
+ it.paddingTop,
86
+ it.paddingRight,
87
+ originalScrollViewPaddingBottom + bottomInset
88
+ )
89
+ }
90
+ }
91
+
92
+ fun clearScrollViewPinning() {
93
+ pinnedScrollView?.setPadding(
94
+ pinnedScrollView!!.paddingLeft,
95
+ pinnedScrollView!!.paddingTop,
96
+ pinnedScrollView!!.paddingRight,
97
+ originalScrollViewPaddingBottom
98
+ )
99
+ pinnedScrollView = null
100
+ originalScrollViewPaddingBottom = 0
101
+ bottomInset = 0
102
+ }
103
+
104
+ fun findScrollView(): ScrollView? = findScrollView(this)
105
+
106
+ private fun findScrollView(view: View): ScrollView? {
107
+ if (view is ScrollView) {
108
+ return view
109
+ }
110
+
111
+ if (view is ViewGroup) {
112
+ for (i in 0 until view.childCount) {
113
+ val scrollView = findScrollView(view.getChildAt(i))
114
+ if (scrollView != null) {
115
+ return scrollView
116
+ }
117
+ }
118
+ }
119
+
120
+ return null
121
+ }
122
+
123
+ // ==================== Keyboard Handling ====================
124
+
125
+ fun setupKeyboardHandler() {
126
+ if (keyboardObserver != null) return
127
+
128
+ keyboardObserver = TrueSheetKeyboardObserver(this, reactContext).apply {
129
+ delegate = object : TrueSheetKeyboardObserverDelegate {
130
+ override fun keyboardWillShow(height: Int) {
131
+ updateScrollViewInsetForKeyboard(height)
132
+ }
133
+
134
+ override fun keyboardDidShow(height: Int) {
135
+ scrollToFocusedInput()
136
+ }
137
+
138
+ override fun keyboardWillHide() {
139
+ updateScrollViewInsetForKeyboard(0)
140
+ }
141
+
142
+ override fun focusDidChange(newFocus: View) {
143
+ scrollToFocusedInput()
144
+ }
145
+ }
146
+ start()
147
+ }
148
+ }
149
+
150
+ fun cleanupKeyboardHandler() {
151
+ keyboardObserver?.stop()
152
+ keyboardObserver = null
153
+ }
154
+
155
+ private fun updateScrollViewInsetForKeyboard(keyboardHeight: Int) {
156
+ val scrollView = pinnedScrollView ?: return
157
+
158
+ val totalBottomInset = if (keyboardHeight > 0) keyboardHeight else bottomInset
159
+ val newPaddingBottom = originalScrollViewPaddingBottom + totalBottomInset
160
+
161
+ scrollView.clipToPadding = false
162
+ scrollView.setPadding(
163
+ scrollView.paddingLeft,
164
+ scrollView.paddingTop,
165
+ scrollView.paddingRight,
166
+ newPaddingBottom
167
+ )
168
+
169
+ // Trigger a scroll to force update
170
+ scrollView.post {
171
+ scrollView.smoothScrollBy(0, 1)
172
+ scrollView.smoothScrollBy(0, -1)
173
+ }
174
+ }
175
+
176
+ private fun scrollToFocusedInput() {
177
+ val scrollView = pinnedScrollView ?: findScrollView() ?: return
178
+ val focusedView = findFocus() ?: return
179
+
180
+ val focusedLocation = IntArray(2)
181
+ val scrollViewLocation = IntArray(2)
182
+ focusedView.getLocationOnScreen(focusedLocation)
183
+ scrollView.getLocationOnScreen(scrollViewLocation)
184
+
185
+ val relativeTop = focusedLocation[1] - scrollViewLocation[1] + scrollView.scrollY
186
+ val relativeBottom = relativeTop + focusedView.height + keyboardScrollOffset.toInt()
187
+
188
+ val visibleHeight = scrollView.height - scrollView.paddingBottom
189
+ val visibleBottom = scrollView.scrollY + visibleHeight
190
+
191
+ if (relativeBottom > visibleBottom) {
192
+ scrollView.smoothScrollTo(0, relativeBottom - visibleHeight)
193
+ }
194
+ }
195
+
35
196
  companion object {
36
197
  const val TAG_NAME = "TrueSheet"
37
198
  }
@@ -1,6 +1,6 @@
1
1
  package com.lodev09.truesheet
2
2
 
3
- import com.facebook.react.TurboReactPackage
3
+ import com.facebook.react.BaseReactPackage
4
4
  import com.facebook.react.bridge.NativeModule
5
5
  import com.facebook.react.bridge.ReactApplicationContext
6
6
  import com.facebook.react.module.model.ReactModuleInfo
@@ -11,7 +11,7 @@ import com.facebook.react.uimanager.ViewManager
11
11
  * TrueSheet package for Fabric architecture
12
12
  * Registers all view managers and the TurboModule
13
13
  */
14
- class TrueSheetPackage : TurboReactPackage() {
14
+ class TrueSheetPackage : BaseReactPackage() {
15
15
 
16
16
  override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
17
17
  when (name) {
@@ -27,7 +27,6 @@ class TrueSheetPackage : TurboReactPackage() {
27
27
  TrueSheetModule::class.java.name,
28
28
  false, // canOverrideExistingModule
29
29
  false, // needsEagerInit
30
- true, // hasConstants
31
30
  false, // isCxxModule
32
31
  true // isTurboModule
33
32
  )
@@ -6,6 +6,7 @@ import android.view.ViewGroup
6
6
  import android.view.accessibility.AccessibilityEvent
7
7
  import androidx.annotation.UiThread
8
8
  import com.facebook.react.bridge.LifecycleEventListener
9
+ import com.facebook.react.bridge.ReadableMap
9
10
  import com.facebook.react.bridge.WritableNativeMap
10
11
  import com.facebook.react.uimanager.PixelUtil.pxToDp
11
12
  import com.facebook.react.uimanager.StateWrapper
@@ -15,6 +16,8 @@ import com.facebook.react.uimanager.events.EventDispatcher
15
16
  import com.facebook.react.util.RNLog
16
17
  import com.facebook.react.views.view.ReactViewGroup
17
18
  import com.lodev09.truesheet.core.GrabberOptions
19
+ import com.lodev09.truesheet.core.RNScreensEventObserver
20
+ import com.lodev09.truesheet.core.RNScreensEventObserverDelegate
18
21
  import com.lodev09.truesheet.core.TrueSheetStackManager
19
22
  import com.lodev09.truesheet.events.*
20
23
  import com.lodev09.truesheet.utils.KeyboardUtils
@@ -29,7 +32,8 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
29
32
  ReactViewGroup(reactContext),
30
33
  LifecycleEventListener,
31
34
  TrueSheetViewControllerDelegate,
32
- TrueSheetContainerViewDelegate {
35
+ TrueSheetContainerViewDelegate,
36
+ RNScreensEventObserverDelegate {
33
37
 
34
38
  // ==================== Properties ====================
35
39
 
@@ -60,6 +64,9 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
60
64
  // Root container for the coordinator layout (activity or Modal dialog content view)
61
65
  internal var rootContainerView: ViewGroup? = null
62
66
 
67
+ // Screen event observer for react-native-screens integration
68
+ internal var screensEventObserver: RNScreensEventObserver? = null
69
+
63
70
  // ==================== Initialization ====================
64
71
 
65
72
  init {
@@ -113,6 +120,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
113
120
  if (child is TrueSheetContainerView) {
114
121
  child.delegate = this
115
122
  viewController.createSheet()
123
+ setupContentScrollViewPinning()
116
124
 
117
125
  val surfaceId = UIManagerHelper.getSurfaceId(this)
118
126
  eventDispatcher?.dispatchEvent(MountEvent(surfaceId, id))
@@ -160,6 +168,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
160
168
  TrueSheetModule.unregisterView(id)
161
169
  TrueSheetStackManager.removeSheet(this)
162
170
 
171
+ cleanupScreenEventObserver()
163
172
  didInitiallyPresent = false
164
173
 
165
174
  if (viewController.isPresented) {
@@ -177,6 +186,8 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
177
186
  * Reconfigures the sheet if it's currently presented.
178
187
  */
179
188
  fun finalizeUpdates() {
189
+ setupContentScrollViewPinning()
190
+
180
191
  if (viewController.isPresented) {
181
192
  viewController.sheetView?.setupBackground()
182
193
  viewController.sheetView?.setupGrabber()
@@ -245,10 +256,51 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
245
256
 
246
257
  fun setInsetAdjustment(insetAdjustment: String) {
247
258
  viewController.insetAdjustment = insetAdjustment
259
+ setupContentScrollViewPinning()
248
260
  }
249
261
 
250
262
  fun setScrollable(scrollable: Boolean) {
251
263
  viewController.scrollable = scrollable
264
+ setupContentScrollViewPinning()
265
+ }
266
+
267
+ fun setScrollableOptions(options: ReadableMap?) {
268
+ viewController.scrollableOptions = options
269
+ setupContentScrollViewPinning()
270
+ }
271
+
272
+ private fun setupContentScrollViewPinning() {
273
+ viewController.containerView?.let {
274
+ it.insetAdjustment = viewController.insetAdjustment
275
+ it.scrollViewPinningEnabled = viewController.scrollable
276
+ it.scrollViewBottomInset = viewController.contentBottomInset
277
+ it.scrollableOptions = viewController.scrollableOptions
278
+ it.setupContentScrollViewPinning()
279
+ }
280
+ }
281
+
282
+ // ==================== Screen Event Observer ====================
283
+
284
+ private fun setupScreenEventObserver() {
285
+ screensEventObserver = RNScreensEventObserver().apply {
286
+ delegate = this@TrueSheetView
287
+
288
+ // For stacked sheets on the same screen, inherit parent's presenter screen tag.
289
+ // If parent was hidden by screen navigation, this sheet is on a different screen.
290
+ val parentScreenTag = viewController.parentSheetView?.screensEventObserver?.presenterScreenTag ?: 0
291
+ val parentHiddenByScreen = viewController.parentSheetView?.viewController?.wasHiddenByScreen == true
292
+ if (parentScreenTag != 0 && !parentHiddenByScreen) {
293
+ presenterScreenTag = parentScreenTag
294
+ } else {
295
+ capturePresenterScreenFromView(this@TrueSheetView)
296
+ }
297
+ startObserving(eventDispatcher)
298
+ }
299
+ }
300
+
301
+ private fun cleanupScreenEventObserver() {
302
+ screensEventObserver?.stopObserving()
303
+ screensEventObserver = null
252
304
  }
253
305
 
254
306
  // ==================== State Management ====================
@@ -328,13 +380,8 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
328
380
 
329
381
  @UiThread
330
382
  fun resize(detentIndex: Int, promiseCallback: () -> Unit) {
331
- if (!viewController.isPresented) {
332
- RNLog.w(reactContext, "TrueSheet: Cannot resize. Sheet is not presented.")
333
- promiseCallback()
334
- return
335
- }
336
-
337
- present(detentIndex, true, promiseCallback)
383
+ viewController.resizePromise = promiseCallback
384
+ viewController.resize(detentIndex)
338
385
  }
339
386
 
340
387
  /**
@@ -409,6 +456,8 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
409
456
  }
410
457
 
411
458
  override fun viewControllerDidPresent(index: Int, position: Float, detent: Float) {
459
+ setupScreenEventObserver()
460
+
412
461
  val surfaceId = UIManagerHelper.getSurfaceId(this)
413
462
  eventDispatcher?.dispatchEvent(DidPresentEvent(surfaceId, id, index, position, detent))
414
463
  }
@@ -423,6 +472,8 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
423
472
  viewController.coordinatorLayout?.let { rootContainerView?.removeView(it) }
424
473
  rootContainerView = null
425
474
 
475
+ cleanupScreenEventObserver()
476
+
426
477
  val surfaceId = UIManagerHelper.getSurfaceId(this)
427
478
  eventDispatcher?.dispatchEvent(DidDismissEvent(surfaceId, id))
428
479
 
@@ -502,6 +553,23 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
502
553
  viewController.positionFooter()
503
554
  }
504
555
 
556
+ // ==================== RNScreensEventObserverDelegate ====================
557
+
558
+ override fun presenterScreenWillDisappear() {
559
+ if (viewController.isPresented && viewController.isSheetVisible) {
560
+ KeyboardUtils.dismiss(this) {}
561
+ viewController.post { viewController.hideForScreen() }
562
+ }
563
+ }
564
+
565
+ override fun presenterScreenWillAppear() {
566
+ if (viewController.isPresented && viewController.wasHiddenByScreen) {
567
+ viewController.wasHiddenByScreen = false
568
+ viewController.showAfterScreen()
569
+ viewControllerDidDetectScreenDismiss()
570
+ }
571
+ }
572
+
505
573
  // ==================== Private Helpers ====================
506
574
 
507
575
  /**