@lodev09/react-native-true-sheet 3.9.6 → 3.9.7

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.
@@ -11,6 +11,7 @@ interface TrueSheetContainerViewDelegate {
11
11
  val eventDispatcher: EventDispatcher?
12
12
  fun containerViewContentDidChangeSize(width: Int, height: Int)
13
13
  fun containerViewContentDidScroll()
14
+ fun containerViewScrollViewDidChange()
14
15
  fun containerViewHeaderDidChangeSize(width: Int, height: Int)
15
16
  fun containerViewFooterDidChangeSize(width: Int, height: Int)
16
17
  }
@@ -124,6 +125,10 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
124
125
  delegate?.containerViewContentDidScroll()
125
126
  }
126
127
 
128
+ override fun contentViewScrollViewDidChange() {
129
+ delegate?.containerViewScrollViewDidChange()
130
+ }
131
+
127
132
  override fun headerViewDidChangeSize(width: Int, height: Int) {
128
133
  headerHeight = height
129
134
  delegate?.containerViewHeaderDidChangeSize(width, height)
@@ -18,6 +18,7 @@ import com.lodev09.truesheet.utils.isDescendantOf
18
18
  interface TrueSheetContentViewDelegate {
19
19
  fun contentViewDidChangeSize(width: Int, height: Int)
20
20
  fun contentViewDidScroll()
21
+ fun contentViewScrollViewDidChange()
21
22
  }
22
23
 
23
24
  /**
@@ -32,7 +33,6 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
32
33
  private var lastHeight = 0
33
34
 
34
35
  private var pinnedScrollView: ScrollView? = null
35
- private var cachedScrollView: ScrollView? = null
36
36
  private var originalScrollViewPaddingBottom: Int = 0
37
37
  private var bottomInset: Int = 0
38
38
 
@@ -45,6 +45,22 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
45
45
  keyboardScrollOffset = value?.getDouble("keyboardScrollOffset")?.toFloat()?.dpToPx() ?: 0f
46
46
  }
47
47
 
48
+ override fun addView(child: View?, index: Int) {
49
+ super.addView(child, index)
50
+ checkScrollViewChanged()
51
+ }
52
+
53
+ override fun removeViewAt(index: Int) {
54
+ super.removeViewAt(index)
55
+ checkScrollViewChanged()
56
+ }
57
+
58
+ private fun checkScrollViewChanged() {
59
+ if (pinnedScrollView == null || pinnedScrollView?.isDescendantOf(this) == false) {
60
+ delegate?.contentViewScrollViewDidChange()
61
+ }
62
+ }
63
+
48
64
  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
49
65
  super.onSizeChanged(w, h, oldw, oldh)
50
66
 
@@ -61,65 +77,63 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
61
77
  return
62
78
  }
63
79
 
64
- this.bottomInset = bottomInset
65
- applyScrollViewBottomInset()
66
- }
80
+ // Check if pinned scroll view is still valid (still in view hierarchy)
81
+ if (pinnedScrollView != null && pinnedScrollView?.isDescendantOf(this) == false) {
82
+ clearScrollable()
83
+ }
67
84
 
68
- private fun applyScrollViewBottomInset() {
69
- val scrollView = findScrollView(this)
85
+ // Already set up with same inset and valid scroll view
86
+ if (pinnedScrollView != null && this.bottomInset == bottomInset) {
87
+ return
88
+ }
70
89
 
71
- if (scrollView != pinnedScrollView) {
72
- // Clean up previous scroll view
73
- pinnedScrollView?.setOnScrollChangeListener(null as View.OnScrollChangeListener?)
74
- pinnedScrollView?.setPadding(
75
- pinnedScrollView!!.paddingLeft,
76
- pinnedScrollView!!.paddingTop,
77
- pinnedScrollView!!.paddingRight,
78
- originalScrollViewPaddingBottom
79
- )
90
+ val scrollView = findScrollView(this) ?: return
80
91
 
92
+ // Only capture originals on first pin
93
+ if (pinnedScrollView == null) {
94
+ originalScrollViewPaddingBottom = scrollView.paddingBottom
81
95
  pinnedScrollView = scrollView
82
- originalScrollViewPaddingBottom = scrollView?.paddingBottom ?: 0
83
96
 
84
- scrollView?.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
97
+ scrollView.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
85
98
  if (scrollY != oldScrollY) {
86
99
  delegate?.contentViewDidScroll()
87
100
  }
88
101
  }
89
102
  }
90
103
 
91
- scrollView?.let {
92
- it.clipToPadding = false
93
- it.setPadding(
94
- it.paddingLeft,
95
- it.paddingTop,
96
- it.paddingRight,
97
- originalScrollViewPaddingBottom + bottomInset
98
- )
104
+ this.bottomInset = bottomInset
105
+
106
+ setScrollViewPaddingBottom(originalScrollViewPaddingBottom + bottomInset)
107
+
108
+ // If keyboard is currently showing, re-apply the keyboard inset to the new ScrollView
109
+ val keyboardHeight = keyboardObserver?.currentHeight ?: 0
110
+ if (keyboardHeight > 0) {
111
+ setScrollViewPaddingBottom(originalScrollViewPaddingBottom + keyboardHeight)
99
112
  }
100
113
  }
101
114
 
115
+ private fun setScrollViewPaddingBottom(paddingBottom: Int) {
116
+ val scrollView = pinnedScrollView ?: return
117
+ scrollView.clipToPadding = false
118
+ scrollView.setPadding(
119
+ scrollView.paddingLeft,
120
+ scrollView.paddingTop,
121
+ scrollView.paddingRight,
122
+ paddingBottom
123
+ )
124
+ }
125
+
102
126
  fun clearScrollable() {
103
127
  pinnedScrollView?.setOnScrollChangeListener(null as View.OnScrollChangeListener?)
104
- pinnedScrollView?.setPadding(
105
- pinnedScrollView!!.paddingLeft,
106
- pinnedScrollView!!.paddingTop,
107
- pinnedScrollView!!.paddingRight,
108
- originalScrollViewPaddingBottom
109
- )
128
+ setScrollViewPaddingBottom(originalScrollViewPaddingBottom)
110
129
  pinnedScrollView = null
111
- cachedScrollView = null
112
130
  originalScrollViewPaddingBottom = 0
113
131
  bottomInset = 0
114
132
  }
115
133
 
116
134
  fun findScrollView(): ScrollView? {
117
- // Return cached if still valid (attached and descendant of this view)
118
- cachedScrollView?.let {
119
- if (it.isAttachedToWindow && it.isDescendantOf(this)) return it
120
- cachedScrollView = null
121
- }
122
- return findScrollView(this as View).also { cachedScrollView = it }
135
+ if (pinnedScrollView != null) return pinnedScrollView
136
+ return findScrollView(this as View)
123
137
  }
124
138
 
125
139
  private fun findScrollView(view: View): ScrollView? {
@@ -175,15 +189,7 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
175
189
  val scrollView = pinnedScrollView ?: return
176
190
 
177
191
  val totalBottomInset = if (keyboardHeight > 0) keyboardHeight else bottomInset
178
- val newPaddingBottom = originalScrollViewPaddingBottom + totalBottomInset
179
-
180
- scrollView.clipToPadding = false
181
- scrollView.setPadding(
182
- scrollView.paddingLeft,
183
- scrollView.paddingTop,
184
- scrollView.paddingRight,
185
- newPaddingBottom
186
- )
192
+ setScrollViewPaddingBottom(originalScrollViewPaddingBottom + totalBottomInset)
187
193
 
188
194
  // Trigger a scroll to force update
189
195
  scrollView.post {
@@ -592,6 +592,13 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
592
592
  viewController.commitKeyboardDetent()
593
593
  }
594
594
 
595
+ // When the ScrollView changes (e.g. conditional remount), re-pin the new ScrollView
596
+ // and request layout so BottomSheetBehavior re-discovers the nested scrolling child.
597
+ override fun containerViewScrollViewDidChange() {
598
+ setupScrollable()
599
+ viewController.sheetView?.requestLayout()
600
+ }
601
+
595
602
  override fun containerViewHeaderDidChangeSize(width: Int, height: Int) {
596
603
  updateSheetIfNeeded()
597
604
  }
@@ -65,15 +65,10 @@ class TrueSheetCoordinatorLayout(context: Context) :
65
65
  get() = PointerEvents.BOX_NONE
66
66
 
67
67
  /**
68
- * Clears stale `nestedScrollingChildRef` from BottomSheetBehavior and forces re-discovery.
68
+ * Clears stale `nestedScrollingChildRef` from BottomSheetBehavior.
69
69
  *
70
- * `BottomSheetBehavior.onLayoutChild` calls `findScrollingChild()` which traverses the
71
- * entire sheet subtree. The cached `nestedScrollingChildRef` can become stale when:
72
- * 1. A child sheet with a ScrollView is dismissed and its ScrollView returns to this hierarchy
73
- * 2. A ScrollView is conditionally removed and re-added (e.g. React conditional rendering)
74
- *
75
- * When stale (GC'd target or view no longer in sheet), we clear the ref and request layout
76
- * so `onLayoutChild` re-runs `findScrollingChild()` to discover the current ScrollView.
70
+ * The cached `nestedScrollingChildRef` can become stale when a child sheet
71
+ * with a ScrollView is dismissed and its ScrollView returns to this hierarchy.
77
72
  */
78
73
  private fun clearStaleNestedScrollingChildRef() {
79
74
  val sheet = delegate?.findSheetView() ?: return
@@ -86,7 +81,6 @@ class TrueSheetCoordinatorLayout(context: Context) :
86
81
  val view = ref.get()
87
82
  if (view == null || !view.isDescendantOf(sheet)) {
88
83
  ref.clear()
89
- sheet.requestLayout()
90
84
  }
91
85
  } catch (_: Exception) {}
92
86
  }
@@ -15,18 +15,11 @@ NS_ASSUME_NONNULL_BEGIN
15
15
 
16
16
  @protocol TrueSheetContainerViewDelegate <NSObject>
17
17
 
18
- /**
19
- * Called when the container's content size changes
20
- * @param newSize The new size of the content
21
- */
22
18
  - (void)containerViewContentDidChangeSize:(CGSize)newSize;
19
+ - (void)containerViewScrollViewDidChange;
23
20
 
24
21
  @optional
25
22
 
26
- /**
27
- * Called when the header size changes
28
- * @param newSize The new size of the header
29
- */
30
23
  - (void)containerViewHeaderDidChangeSize:(CGSize)newSize;
31
24
 
32
25
  @end
@@ -175,8 +175,8 @@ using namespace facebook::react;
175
175
  [self.delegate containerViewContentDidChangeSize:newSize];
176
176
  }
177
177
 
178
- - (void)contentViewDidChangeChildren {
179
- [self setupScrollable];
178
+ - (void)contentViewScrollViewDidChange {
179
+ [self.delegate containerViewScrollViewDidChange];
180
180
  }
181
181
 
182
182
  #pragma mark - TrueSheetHeaderViewDelegate
@@ -22,7 +22,7 @@ NS_ASSUME_NONNULL_BEGIN
22
22
  @protocol TrueSheetContentViewDelegate <NSObject>
23
23
 
24
24
  - (void)contentViewDidChangeSize:(CGSize)newSize;
25
- - (void)contentViewDidChangeChildren;
25
+ - (void)contentViewScrollViewDidChange;
26
26
 
27
27
  @end
28
28
 
@@ -57,29 +57,42 @@ using namespace facebook::react;
57
57
 
58
58
  - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
59
59
  [super mountChildComponentView:childComponentView index:index];
60
- [self.delegate contentViewDidChangeChildren];
60
+ [self checkScrollViewChanged];
61
61
  }
62
62
 
63
63
  - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
64
64
  [super unmountChildComponentView:childComponentView index:index];
65
- [self.delegate contentViewDidChangeChildren];
65
+ [self checkScrollViewChanged];
66
+ }
67
+
68
+ - (void)checkScrollViewChanged {
69
+ if (!_pinnedScrollView || ![_pinnedScrollView isDescendantOfView:self]) {
70
+ [self.delegate contentViewScrollViewDidChange];
71
+ }
66
72
  }
67
73
 
68
74
  #pragma mark - Scrollable
69
75
 
76
+ - (void)setScrollViewContentInset:(CGFloat)contentBottom indicatorInset:(CGFloat)indicatorBottom {
77
+ if (!_pinnedScrollView)
78
+ return;
79
+
80
+ UIEdgeInsets contentInset = _pinnedScrollView.scrollView.contentInset;
81
+ contentInset.bottom = contentBottom;
82
+ _pinnedScrollView.scrollView.contentInset = contentInset;
83
+
84
+ UIEdgeInsets indicatorInsets = _pinnedScrollView.scrollView.verticalScrollIndicatorInsets;
85
+ indicatorInsets.bottom = indicatorBottom;
86
+ _pinnedScrollView.scrollView.verticalScrollIndicatorInsets = indicatorInsets;
87
+ }
88
+
70
89
  - (void)clearScrollable {
71
90
  if (_pinnedScrollView) {
72
91
  CGRect frame = _pinnedScrollView.frame;
73
92
  frame.size.height = _originalScrollViewHeight;
74
93
  _pinnedScrollView.frame = frame;
75
94
 
76
- UIEdgeInsets contentInset = _pinnedScrollView.scrollView.contentInset;
77
- contentInset.bottom = 0;
78
- _pinnedScrollView.scrollView.contentInset = contentInset;
79
-
80
- UIEdgeInsets indicatorInsets = _pinnedScrollView.scrollView.verticalScrollIndicatorInsets;
81
- indicatorInsets.bottom = _originalIndicatorBottomInset;
82
- _pinnedScrollView.scrollView.verticalScrollIndicatorInsets = indicatorInsets;
95
+ [self setScrollViewContentInset:0 indicatorInset:_originalIndicatorBottomInset];
83
96
  }
84
97
  _pinnedScrollView = nil;
85
98
  _bottomInset = 0;
@@ -94,7 +107,6 @@ using namespace facebook::react;
94
107
  }
95
108
 
96
109
  // Check if pinned scroll view is still valid (still in view hierarchy)
97
- // This handles the case where content changes and the old scroll view is unmounted
98
110
  if (_pinnedScrollView && ![_pinnedScrollView isDescendantOfView:self]) {
99
111
  [self clearScrollable];
100
112
  }
@@ -120,13 +132,13 @@ using namespace facebook::react;
120
132
 
121
133
  [self updateScrollViewHeight];
122
134
 
123
- UIEdgeInsets contentInset = scrollView.scrollView.contentInset;
124
- contentInset.bottom = bottomInset;
125
- scrollView.scrollView.contentInset = contentInset;
135
+ [self setScrollViewContentInset:_bottomInset indicatorInset:_originalIndicatorBottomInset];
126
136
 
127
- UIEdgeInsets indicatorInsets = scrollView.scrollView.verticalScrollIndicatorInsets;
128
- indicatorInsets.bottom = _originalIndicatorBottomInset;
129
- scrollView.scrollView.verticalScrollIndicatorInsets = indicatorInsets;
137
+ // If keyboard is currently showing, re-apply the keyboard inset to the new ScrollView
138
+ CGFloat keyboardHeight = _keyboardObserver ? _keyboardObserver.currentHeight : 0;
139
+ if (keyboardHeight > 0) {
140
+ [self setScrollViewContentInset:keyboardHeight indicatorInset:_originalIndicatorBottomInset + keyboardHeight];
141
+ }
130
142
  }
131
143
 
132
144
  - (void)updateScrollViewHeight {
@@ -202,13 +214,8 @@ using namespace facebook::react;
202
214
  delay:0
203
215
  options:curve | UIViewAnimationOptionBeginFromCurrentState
204
216
  animations:^{
205
- UIEdgeInsets contentInset = self->_pinnedScrollView.scrollView.contentInset;
206
- contentInset.bottom = height;
207
- self->_pinnedScrollView.scrollView.contentInset = contentInset;
208
-
209
- UIEdgeInsets indicatorInsets = self->_pinnedScrollView.scrollView.verticalScrollIndicatorInsets;
210
- indicatorInsets.bottom = self->_originalIndicatorBottomInset + height;
211
- self->_pinnedScrollView.scrollView.verticalScrollIndicatorInsets = indicatorInsets;
217
+ [self setScrollViewContentInset:height
218
+ indicatorInset:self->_originalIndicatorBottomInset + height];
212
219
 
213
220
  if (firstResponder) {
214
221
  CGRect responderFrame = [firstResponder convertRect:firstResponder.bounds
@@ -229,13 +236,8 @@ using namespace facebook::react;
229
236
  delay:0
230
237
  options:curve | UIViewAnimationOptionBeginFromCurrentState
231
238
  animations:^{
232
- UIEdgeInsets contentInset = self->_pinnedScrollView.scrollView.contentInset;
233
- contentInset.bottom = self->_bottomInset;
234
- self->_pinnedScrollView.scrollView.contentInset = contentInset;
235
-
236
- UIEdgeInsets indicatorInsets = self->_pinnedScrollView.scrollView.verticalScrollIndicatorInsets;
237
- indicatorInsets.bottom = self->_originalIndicatorBottomInset;
238
- self->_pinnedScrollView.scrollView.verticalScrollIndicatorInsets = indicatorInsets;
239
+ [self setScrollViewContentInset:self->_bottomInset
240
+ indicatorInset:self->_originalIndicatorBottomInset];
239
241
  }
240
242
  completion:nil];
241
243
  }
@@ -574,6 +574,11 @@ using namespace facebook::react;
574
574
  [self setupSheetDetentsForSizeChange];
575
575
  }
576
576
 
577
+ // When the ScrollView changes (e.g. conditional remount), re-pin the new ScrollView.
578
+ - (void)containerViewScrollViewDidChange {
579
+ [_containerView setupScrollable];
580
+ }
581
+
577
582
  #pragma mark - TrueSheetViewControllerDelegate
578
583
 
579
584
  - (void)viewControllerWillPresentAtIndex:(NSInteger)index position:(CGFloat)position detent:(CGFloat)detent {
@@ -24,6 +24,7 @@ NS_ASSUME_NONNULL_BEGIN
24
24
  @interface TrueSheetKeyboardObserver : NSObject
25
25
 
26
26
  @property (nonatomic, weak, nullable) TrueSheetViewController *viewController;
27
+ @property (nonatomic, readonly) CGFloat currentHeight;
27
28
 
28
29
  - (void)addDelegate:(id<TrueSheetKeyboardObserverDelegate>)delegate;
29
30
  - (void)removeDelegate:(id<TrueSheetKeyboardObserverDelegate>)delegate;
@@ -14,6 +14,11 @@
14
14
 
15
15
  @implementation TrueSheetKeyboardObserver {
16
16
  NSHashTable<id<TrueSheetKeyboardObserverDelegate>> *_delegates;
17
+ CGFloat _currentHeight;
18
+ }
19
+
20
+ - (CGFloat)currentHeight {
21
+ return _currentHeight;
17
22
  }
18
23
 
19
24
  - (instancetype)init {
@@ -76,6 +81,8 @@
76
81
  CGRect keyboardFrameInWindow = [window convertRect:keyboardFrame fromWindow:nil];
77
82
  CGFloat keyboardHeight = MAX(0, window.bounds.size.height - keyboardFrameInWindow.origin.y);
78
83
 
84
+ _currentHeight = keyboardHeight;
85
+
79
86
  for (id<TrueSheetKeyboardObserverDelegate> delegate in _delegates) {
80
87
  if (keyboardHeight > 0) {
81
88
  [delegate keyboardWillShow:keyboardHeight duration:duration curve:curve];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lodev09/react-native-true-sheet",
3
- "version": "3.9.6",
3
+ "version": "3.9.7",
4
4
  "description": "The true native bottom sheet experience for your React Native Apps.",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./lib/module/index.js",
@@ -188,7 +188,8 @@
188
188
  "before:init": [
189
189
  "yarn tidy",
190
190
  "yarn test"
191
- ]
191
+ ],
192
+ "after:bump": "sed -i '' 's/## Unreleased/## Unreleased\\n\\n## ${version}/' CHANGELOG.md && git add CHANGELOG.md"
192
193
  },
193
194
  "github": {
194
195
  "release": true,