@lodev09/react-native-true-sheet 4.0.0-beta.3 → 4.0.0-beta.4
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.
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +28 -4
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +206 -149
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +5 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +6 -17
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +50 -7
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +101 -18
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +24 -1
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +3 -3
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +14 -24
- package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +0 -7
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +50 -4
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +7 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +2 -1
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +11 -3
- package/ios/TrueSheetContainerView.h +13 -5
- package/ios/TrueSheetContainerView.mm +6 -6
- package/ios/TrueSheetContentView.h +31 -10
- package/ios/TrueSheetContentView.mm +137 -162
- package/ios/TrueSheetFooterView.mm +17 -0
- package/ios/TrueSheetView.mm +13 -4
- package/ios/utils/UIView+ScrollEdgeInteraction.h +1 -0
- package/ios/utils/UIView+ScrollEdgeInteraction.mm +24 -2
- package/lib/module/TrueSheet.js +37 -9
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +26 -24
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +4 -0
- package/lib/typescript/src/TrueSheet.d.ts +2 -0
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +50 -1
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +3 -0
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/navigation/types.d.ts +1 -1
- package/lib/typescript/src/navigation/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/TrueSheet.tsx +34 -8
- package/src/TrueSheet.types.ts +53 -1
- package/src/TrueSheet.web.tsx +37 -23
- package/src/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
- package/src/fabric/TrueSheetViewNativeComponent.ts +4 -0
- package/src/navigation/types.ts +1 -0
|
@@ -17,25 +17,21 @@
|
|
|
17
17
|
#import <react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h>
|
|
18
18
|
#import "TrueSheetContainerView.h"
|
|
19
19
|
#import "TrueSheetFooterView.h"
|
|
20
|
-
#import "TrueSheetView.h"
|
|
21
20
|
#import "TrueSheetViewController.h"
|
|
22
21
|
#import "utils/PlatformUtil.h"
|
|
23
22
|
#import "utils/UIView+FirstResponder.h"
|
|
24
23
|
|
|
25
24
|
using namespace facebook::react;
|
|
26
25
|
|
|
27
|
-
static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
|
|
28
|
-
|
|
29
26
|
@implementation TrueSheetContentView {
|
|
30
27
|
TrueSheetContentViewShadowNode::ConcreteState::Shared _state;
|
|
31
|
-
RCTScrollViewComponentView *
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
CGFloat _bottomInset;
|
|
35
|
-
CGFloat _originalIndicatorBottomInset;
|
|
28
|
+
RCTScrollViewComponentView *_detectedScrollView;
|
|
29
|
+
CGFloat _lastReportedNaturalHeight;
|
|
30
|
+
CGFloat _appliedKeyboardOffset;
|
|
36
31
|
BOOL _observingTextChanges;
|
|
37
32
|
BOOL _scrollableBounded;
|
|
38
|
-
|
|
33
|
+
UIScrollViewContentInsetAdjustmentBehavior _originalInsetAdjustmentBehavior;
|
|
34
|
+
BOOL _appliedContentInsetAdjustment;
|
|
39
35
|
}
|
|
40
36
|
|
|
41
37
|
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
@@ -52,15 +48,20 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
|
|
|
52
48
|
|
|
53
49
|
- (void)dealloc {
|
|
54
50
|
[self stopObservingTextChanges];
|
|
55
|
-
[self unobserveScrollViewContentSize];
|
|
56
51
|
}
|
|
57
52
|
|
|
58
53
|
- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState {
|
|
59
54
|
_state = std::static_pointer_cast<TrueSheetContentViewShadowNode::ConcreteState const>(state);
|
|
55
|
+
|
|
56
|
+
CGFloat naturalHeight = _state->getData().naturalHeight;
|
|
57
|
+
if (naturalHeight != _lastReportedNaturalHeight) {
|
|
58
|
+
_lastReportedNaturalHeight = naturalHeight;
|
|
59
|
+
[self.delegate contentViewDidChangeSize:CGSizeMake(self.frame.size.width, naturalHeight)];
|
|
60
|
+
}
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
// Tells the shadow node to fill the container (flexGrow/flexShrink) so the
|
|
63
|
-
//
|
|
64
|
+
// detected ScrollView's viewport is bounded to the visible space. Only applied
|
|
64
65
|
// for auto detents — otherwise content lays out naturally like a regular view.
|
|
65
66
|
- (void)setScrollableBounded:(BOOL)bounded {
|
|
66
67
|
if (_scrollableBounded == bounded) {
|
|
@@ -69,19 +70,55 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
|
|
|
69
70
|
_scrollableBounded = bounded;
|
|
70
71
|
|
|
71
72
|
if (_state) {
|
|
72
|
-
|
|
73
|
+
auto newState = _state->getData();
|
|
73
74
|
newState.scrollableBounded = bounded;
|
|
74
75
|
_state->updateState(std::move(newState));
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
- (void)setScrollableHandle:(NSInteger)scrollableHandle {
|
|
80
|
+
if (_scrollableHandle == scrollableHandle) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
_scrollableHandle = scrollableHandle;
|
|
84
|
+
|
|
85
|
+
// Release the previously resolved ScrollView — setupScrollable re-resolves
|
|
86
|
+
[self clearScrollable];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
- (void)setContentInsetAdjustment:(BOOL)contentInsetAdjustment {
|
|
90
|
+
if (_contentInsetAdjustment == contentInsetAdjustment) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
_contentInsetAdjustment = contentInsetAdjustment;
|
|
94
|
+
[self applyContentInsetAdjustment];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Hands the bottom safe-area inset to UIKit — `automatic` only insets while
|
|
98
|
+
// the content can scroll, which the Android counterpart mirrors manually.
|
|
99
|
+
- (void)applyContentInsetAdjustment {
|
|
100
|
+
if (!_detectedScrollView) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
UIScrollView *scrollView = _detectedScrollView.scrollView;
|
|
105
|
+
if (_contentInsetAdjustment && !_appliedContentInsetAdjustment) {
|
|
106
|
+
_originalInsetAdjustmentBehavior = scrollView.contentInsetAdjustmentBehavior;
|
|
107
|
+
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAutomatic;
|
|
108
|
+
_appliedContentInsetAdjustment = YES;
|
|
109
|
+
} else if (!_contentInsetAdjustment && _appliedContentInsetAdjustment) {
|
|
110
|
+
scrollView.contentInsetAdjustmentBehavior = _originalInsetAdjustmentBehavior;
|
|
111
|
+
_appliedContentInsetAdjustment = NO;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
78
115
|
- (void)setHasAutoDetent:(BOOL)hasAutoDetent {
|
|
79
116
|
if (_hasAutoDetent == hasAutoDetent) {
|
|
80
117
|
return;
|
|
81
118
|
}
|
|
82
119
|
_hasAutoDetent = hasAutoDetent;
|
|
83
120
|
|
|
84
|
-
if (
|
|
121
|
+
if (_detectedScrollView) {
|
|
85
122
|
[self setScrollableBounded:hasAutoDetent];
|
|
86
123
|
}
|
|
87
124
|
}
|
|
@@ -111,58 +148,25 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
|
|
|
111
148
|
#pragma mark - Layout
|
|
112
149
|
|
|
113
150
|
- (CGFloat)naturalHeight {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
height
|
|
117
|
-
|
|
118
|
-
return MAX(0, height);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
- (void)reportSizeIfChanged {
|
|
122
|
-
// A deep ScrollView unmount doesn't pass through this view's mount hooks —
|
|
123
|
-
// detect the stale pin here so it (and its contentSize observation) is
|
|
124
|
-
// released instead of lingering until the next explicit setup or recycle.
|
|
125
|
-
if (_pinnedScrollView && ![_pinnedScrollView isDescendantOfView:self]) {
|
|
126
|
-
[self.delegate contentViewScrollViewDidChange];
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// naturalHeight mixes frames from different views; mid-transaction they're
|
|
130
|
-
// momentarily inconsistent (parent updates before the ScrollView), which
|
|
131
|
-
// feeds back into the auto detent and oscillates. Coalesce to the next
|
|
132
|
-
// main-queue tick so frames are settled before measuring.
|
|
133
|
-
if (_pinnedScrollView) {
|
|
134
|
-
if (_isReportPending) {
|
|
135
|
-
return;
|
|
151
|
+
if (_state) {
|
|
152
|
+
CGFloat height = _state->getData().naturalHeight;
|
|
153
|
+
if (height > 0) {
|
|
154
|
+
return height;
|
|
136
155
|
}
|
|
137
|
-
_isReportPending = YES;
|
|
138
|
-
|
|
139
|
-
__weak __typeof(self) weakSelf = self;
|
|
140
|
-
dispatch_async(dispatch_get_main_queue(), ^{
|
|
141
|
-
__typeof(self) strongSelf = weakSelf;
|
|
142
|
-
if (!strongSelf) {
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
strongSelf->_isReportPending = NO;
|
|
146
|
-
[strongSelf reportSizeNow];
|
|
147
|
-
});
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
[self reportSizeNow];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
- (void)reportSizeNow {
|
|
155
|
-
CGSize newSize = CGSizeMake(self.frame.size.width, self.naturalHeight);
|
|
156
|
-
if (!CGSizeEqualToSize(newSize, _lastSize)) {
|
|
157
|
-
_lastSize = newSize;
|
|
158
|
-
[self.delegate contentViewDidChangeSize:newSize];
|
|
159
156
|
}
|
|
157
|
+
return self.frame.size.height;
|
|
160
158
|
}
|
|
161
159
|
|
|
162
160
|
- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
|
|
163
161
|
oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics {
|
|
164
162
|
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
|
|
165
|
-
|
|
163
|
+
|
|
164
|
+
// A deep ScrollView unmount doesn't pass through this view's mount hooks —
|
|
165
|
+
// detect the stale reference here so it's released instead of lingering until the
|
|
166
|
+
// next explicit setup or recycle.
|
|
167
|
+
if (_detectedScrollView && ![_detectedScrollView isDescendantOfView:self]) {
|
|
168
|
+
[self.delegate contentViewScrollViewDidChange];
|
|
169
|
+
}
|
|
166
170
|
}
|
|
167
171
|
|
|
168
172
|
#pragma mark - Child Mounting
|
|
@@ -178,46 +182,42 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
|
|
|
178
182
|
}
|
|
179
183
|
|
|
180
184
|
- (void)checkScrollViewChanged {
|
|
181
|
-
if (!
|
|
185
|
+
if (!_detectedScrollView || ![_detectedScrollView isDescendantOfView:self]) {
|
|
182
186
|
[self.delegate contentViewScrollViewDidChange];
|
|
183
187
|
}
|
|
184
188
|
}
|
|
185
189
|
|
|
186
190
|
#pragma mark - Scrollable
|
|
187
191
|
|
|
188
|
-
|
|
189
|
-
|
|
192
|
+
// The scroll indicator follows automatically — UIKit derives its insets from
|
|
193
|
+
// the content inset while `automaticallyAdjustsScrollIndicatorInsets` is set.
|
|
194
|
+
- (void)setKeyboardInset:(CGFloat)inset {
|
|
195
|
+
if (!_detectedScrollView)
|
|
190
196
|
return;
|
|
191
197
|
|
|
192
|
-
UIEdgeInsets contentInset =
|
|
193
|
-
contentInset.bottom =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
UIEdgeInsets indicatorInsets = _pinnedScrollView.scrollView.verticalScrollIndicatorInsets;
|
|
197
|
-
indicatorInsets.bottom = indicatorBottom;
|
|
198
|
-
_pinnedScrollView.scrollView.verticalScrollIndicatorInsets = indicatorInsets;
|
|
198
|
+
UIEdgeInsets contentInset = _detectedScrollView.scrollView.contentInset;
|
|
199
|
+
contentInset.bottom = inset;
|
|
200
|
+
_detectedScrollView.scrollView.contentInset = contentInset;
|
|
199
201
|
}
|
|
200
202
|
|
|
201
203
|
- (void)clearScrollable {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
+
[self setKeyboardInset:0];
|
|
205
|
+
_appliedKeyboardOffset = 0;
|
|
206
|
+
if (_appliedContentInsetAdjustment) {
|
|
207
|
+
_detectedScrollView.scrollView.contentInsetAdjustmentBehavior = _originalInsetAdjustmentBehavior;
|
|
208
|
+
_appliedContentInsetAdjustment = NO;
|
|
204
209
|
}
|
|
205
|
-
[self unobserveScrollViewContentSize];
|
|
206
210
|
[self setScrollableBounded:NO];
|
|
207
|
-
|
|
208
|
-
_bottomInset = 0;
|
|
209
|
-
_originalIndicatorBottomInset = 0;
|
|
210
|
-
[self reportSizeIfChanged];
|
|
211
|
+
_detectedScrollView = nil;
|
|
211
212
|
}
|
|
212
213
|
|
|
213
|
-
- (void)
|
|
214
|
-
// Check if
|
|
215
|
-
if (
|
|
214
|
+
- (void)setupScrollable {
|
|
215
|
+
// Check if the detected scroll view is still valid (still in view hierarchy)
|
|
216
|
+
if (_detectedScrollView && ![_detectedScrollView isDescendantOfView:self]) {
|
|
216
217
|
[self clearScrollable];
|
|
217
218
|
}
|
|
218
219
|
|
|
219
|
-
|
|
220
|
-
if (_pinnedScrollView && _bottomInset == bottomInset) {
|
|
220
|
+
if (_detectedScrollView) {
|
|
221
221
|
return;
|
|
222
222
|
}
|
|
223
223
|
|
|
@@ -226,104 +226,75 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
|
|
|
226
226
|
return;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
|
|
230
|
-
if (!_pinnedScrollView) {
|
|
231
|
-
_originalIndicatorBottomInset = scrollView.scrollView.verticalScrollIndicatorInsets.bottom;
|
|
232
|
-
_pinnedScrollView = scrollView;
|
|
229
|
+
_detectedScrollView = scrollView;
|
|
233
230
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
[self reportSizeIfChanged];
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
_bottomInset = bottomInset;
|
|
240
|
-
|
|
241
|
-
[self setScrollViewContentInset:_bottomInset indicatorInset:_originalIndicatorBottomInset];
|
|
231
|
+
[self applyContentInsetAdjustment];
|
|
232
|
+
[self setScrollableBounded:_hasAutoDetent];
|
|
242
233
|
|
|
243
234
|
// If keyboard is currently showing, re-apply the keyboard inset to the new ScrollView
|
|
244
235
|
CGFloat keyboardHeight = _keyboardObserver ? _keyboardObserver.currentHeight : 0;
|
|
245
236
|
if (keyboardHeight > 0) {
|
|
246
|
-
|
|
247
|
-
[self setScrollViewContentInset:inset indicatorInset:_originalIndicatorBottomInset + inset];
|
|
237
|
+
[self setKeyboardInset:[self keyboardInsetWithHeight:keyboardHeight]];
|
|
248
238
|
}
|
|
249
239
|
}
|
|
250
240
|
|
|
251
|
-
// An absolute footer rises above the keyboard and covers the content's bottom
|
|
252
|
-
// edge — include it so the caret clears the footer, not just the keyboard.
|
|
253
241
|
// A relative footer stays behind the keyboard below the content, so its
|
|
254
|
-
// height shields that much of the keyboard's overlap.
|
|
242
|
+
// height shields that much of the keyboard's overlap. An absolute footer
|
|
243
|
+
// floats within the viewport — its clearance is the content padding's job
|
|
244
|
+
// (the caret reveal accounts for it, see footerOcclusion).
|
|
255
245
|
- (CGFloat)keyboardInsetWithHeight:(CGFloat)height {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (_keyboardObserver.viewController.absoluteFooter) {
|
|
260
|
-
return height + [self.footerView keyboardOcclusionHeight];
|
|
246
|
+
CGFloat inset = height;
|
|
247
|
+
if (self.footerView && !_keyboardObserver.viewController.absoluteFooter) {
|
|
248
|
+
inset = MAX(0, height - self.footerView.frame.size.height);
|
|
261
249
|
}
|
|
262
|
-
return MAX(0, height - self.footerView.frame.size.height);
|
|
263
|
-
}
|
|
264
250
|
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (_observedScrollView == scrollView) {
|
|
270
|
-
return;
|
|
251
|
+
// UIKit stacks the safe area on top of contentInset while the adjustment
|
|
252
|
+
// behavior is automatic — take it out so the total lands on the keyboard edge.
|
|
253
|
+
if (_appliedContentInsetAdjustment) {
|
|
254
|
+
inset = MAX(0, inset - _detectedScrollView.scrollView.safeAreaInsets.bottom);
|
|
271
255
|
}
|
|
272
|
-
[self unobserveScrollViewContentSize];
|
|
273
|
-
_observedScrollView = scrollView;
|
|
274
|
-
[scrollView addObserver:self
|
|
275
|
-
forKeyPath:@"contentSize"
|
|
276
|
-
options:NSKeyValueObservingOptionNew
|
|
277
|
-
context:TrueSheetContentSizeContext];
|
|
278
|
-
}
|
|
279
256
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
257
|
+
// Track how much of keyboardOffset actually lands in the inset so the caret
|
|
258
|
+
// reveal can compensate — the offset shifts the inset, not the keyboard edge.
|
|
259
|
+
CGFloat adjustedInset = MAX(0, inset + self.keyboardOffset);
|
|
260
|
+
_appliedKeyboardOffset = adjustedInset - inset;
|
|
261
|
+
|
|
262
|
+
// Content that already fits above the keyboard has nothing to reveal — the
|
|
263
|
+
// inset would only open blank scroll range below it (huge gap at the bottom).
|
|
264
|
+
UIScrollView *scrollView = _detectedScrollView.scrollView;
|
|
265
|
+
if (scrollView.contentSize.height <= scrollView.bounds.size.height - adjustedInset) {
|
|
266
|
+
_appliedKeyboardOffset = 0;
|
|
267
|
+
return 0;
|
|
284
268
|
}
|
|
269
|
+
|
|
270
|
+
return adjustedInset;
|
|
285
271
|
}
|
|
286
272
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
[self reportSizeIfChanged];
|
|
293
|
-
return;
|
|
273
|
+
// An absolute footer floats over the viewport's bottom edge — extend the
|
|
274
|
+
// caret target so it clears the footer, not just the keyboard.
|
|
275
|
+
- (CGFloat)footerOcclusion {
|
|
276
|
+
if (self.footerView && _keyboardObserver.viewController.absoluteFooter) {
|
|
277
|
+
return [self.footerView keyboardOcclusionHeight];
|
|
294
278
|
}
|
|
295
|
-
|
|
279
|
+
return 0;
|
|
296
280
|
}
|
|
297
281
|
|
|
282
|
+
// Resolves the user-provided `scrollableHandle` within the content subtree —
|
|
283
|
+
// Fabric component views carry their React tag as `UIView.tag`.
|
|
298
284
|
- (RCTScrollViewComponentView *)findScrollView {
|
|
299
|
-
if (
|
|
300
|
-
return
|
|
285
|
+
if (_detectedScrollView) {
|
|
286
|
+
return _detectedScrollView;
|
|
301
287
|
}
|
|
302
288
|
|
|
303
|
-
if (
|
|
289
|
+
if (_scrollableHandle <= 0) {
|
|
304
290
|
return nil;
|
|
305
291
|
}
|
|
306
292
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
for (UIView *subview in self.subviews) {
|
|
311
|
-
scrollView = [self findScrollViewInSubviews:subview.subviews];
|
|
312
|
-
if (scrollView) {
|
|
313
|
-
break;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
293
|
+
UIView *view = [self viewWithTag:_scrollableHandle];
|
|
294
|
+
if ([view isKindOfClass:RCTScrollViewComponentView.class]) {
|
|
295
|
+
return (RCTScrollViewComponentView *)view;
|
|
316
296
|
}
|
|
317
297
|
|
|
318
|
-
return scrollView;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
- (RCTScrollViewComponentView *)findScrollViewInSubviews:(NSArray<UIView *> *)subviews {
|
|
322
|
-
for (UIView *subview in subviews) {
|
|
323
|
-
if ([subview isKindOfClass:RCTScrollViewComponentView.class] && ![subview isKindOfClass:TrueSheetView.class]) {
|
|
324
|
-
return (RCTScrollViewComponentView *)subview;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
298
|
return nil;
|
|
328
299
|
}
|
|
329
300
|
|
|
@@ -331,11 +302,11 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
|
|
|
331
302
|
|
|
332
303
|
- (void)applyScrollEdgeEffects:(nullable ScrollableOptions *)options {
|
|
333
304
|
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(26_0)
|
|
334
|
-
if (!
|
|
305
|
+
if (!_detectedScrollView)
|
|
335
306
|
return;
|
|
336
307
|
|
|
337
308
|
if (@available(iOS 26.0, *)) {
|
|
338
|
-
UIScrollView *scrollView =
|
|
309
|
+
UIScrollView *scrollView = _detectedScrollView.scrollView;
|
|
339
310
|
auto topEffect = options ? options.topScrollEdgeEffect : TrueSheetViewTopScrollEdgeEffect::Hidden;
|
|
340
311
|
auto bottomEffect = options ? options.bottomScrollEdgeEffect : TrueSheetViewBottomScrollEdgeEffect::Hidden;
|
|
341
312
|
|
|
@@ -371,7 +342,7 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
|
|
|
371
342
|
#pragma mark - TrueSheetKeyboardObserverDelegate
|
|
372
343
|
|
|
373
344
|
- (void)keyboardWillShow:(CGFloat)height duration:(NSTimeInterval)duration curve:(UIViewAnimationOptions)curve {
|
|
374
|
-
if (!
|
|
345
|
+
if (!_detectedScrollView) {
|
|
375
346
|
return;
|
|
376
347
|
}
|
|
377
348
|
|
|
@@ -385,7 +356,7 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
|
|
|
385
356
|
delay:0
|
|
386
357
|
options:curve | UIViewAnimationOptionBeginFromCurrentState
|
|
387
358
|
animations:^{
|
|
388
|
-
[self
|
|
359
|
+
[self setKeyboardInset:inset];
|
|
389
360
|
}
|
|
390
361
|
completion:nil];
|
|
391
362
|
|
|
@@ -398,7 +369,7 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
|
|
|
398
369
|
}
|
|
399
370
|
|
|
400
371
|
- (void)focusedInputTextDidChange:(NSNotification *)notification {
|
|
401
|
-
if (!
|
|
372
|
+
if (!_detectedScrollView || !_keyboardObserver || _keyboardObserver.currentHeight <= 0) {
|
|
402
373
|
return;
|
|
403
374
|
}
|
|
404
375
|
|
|
@@ -409,12 +380,15 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
|
|
|
409
380
|
}
|
|
410
381
|
|
|
411
382
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
383
|
+
// Typing can grow the content (e.g. a multiline input) past the fits-above-
|
|
384
|
+
// the-keyboard threshold — keep the inset in sync before revealing the caret.
|
|
385
|
+
[self setKeyboardInset:[self keyboardInsetWithHeight:self->_keyboardObserver.currentHeight]];
|
|
412
386
|
[self scrollToFocusedCaretAnimated:YES];
|
|
413
387
|
});
|
|
414
388
|
}
|
|
415
389
|
|
|
416
390
|
- (void)scrollToFocusedCaretAnimated:(BOOL)animated {
|
|
417
|
-
if (!
|
|
391
|
+
if (!_detectedScrollView) {
|
|
418
392
|
return;
|
|
419
393
|
}
|
|
420
394
|
|
|
@@ -424,7 +398,7 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
|
|
|
424
398
|
return;
|
|
425
399
|
}
|
|
426
400
|
|
|
427
|
-
UIScrollView *scrollView =
|
|
401
|
+
UIScrollView *scrollView = _detectedScrollView.scrollView;
|
|
428
402
|
CGRect targetRect = [firstResponder convertRect:firstResponder.bounds toView:scrollView];
|
|
429
403
|
|
|
430
404
|
if ([firstResponder conformsToProtocol:@protocol(UITextInput)]) {
|
|
@@ -442,14 +416,14 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
|
|
|
442
416
|
}
|
|
443
417
|
}
|
|
444
418
|
|
|
445
|
-
targetRect.size.height += self.keyboardScrollOffset;
|
|
419
|
+
targetRect.size.height += self.keyboardScrollOffset + [self footerOcclusion] - _appliedKeyboardOffset;
|
|
446
420
|
[scrollView scrollRectToVisible:targetRect animated:animated];
|
|
447
421
|
}
|
|
448
422
|
|
|
449
423
|
- (void)keyboardWillHide:(NSTimeInterval)duration curve:(UIViewAnimationOptions)curve {
|
|
450
424
|
[self stopObservingTextChanges];
|
|
451
425
|
|
|
452
|
-
if (!
|
|
426
|
+
if (!_detectedScrollView) {
|
|
453
427
|
return;
|
|
454
428
|
}
|
|
455
429
|
|
|
@@ -457,8 +431,7 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
|
|
|
457
431
|
delay:0
|
|
458
432
|
options:curve | UIViewAnimationOptionBeginFromCurrentState
|
|
459
433
|
animations:^{
|
|
460
|
-
[self
|
|
461
|
-
indicatorInset:self->_originalIndicatorBottomInset];
|
|
434
|
+
[self setKeyboardInset:0];
|
|
462
435
|
}
|
|
463
436
|
completion:nil];
|
|
464
437
|
}
|
|
@@ -471,8 +444,10 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
|
|
|
471
444
|
[self clearScrollable];
|
|
472
445
|
_state.reset();
|
|
473
446
|
_scrollableBounded = NO;
|
|
447
|
+
_scrollableHandle = 0;
|
|
448
|
+
_contentInsetAdjustment = NO;
|
|
474
449
|
_hasAutoDetent = NO;
|
|
475
|
-
|
|
450
|
+
_lastReportedNaturalHeight = 0;
|
|
476
451
|
}
|
|
477
452
|
|
|
478
453
|
@end
|
|
@@ -196,6 +196,7 @@ using namespace facebook::react;
|
|
|
196
196
|
options:curve | UIViewAnimationOptionBeginFromCurrentState
|
|
197
197
|
animations:^{
|
|
198
198
|
self.transform = CGAffineTransformMakeTranslation(0, -slide);
|
|
199
|
+
[self syncEdgeEffect];
|
|
199
200
|
}
|
|
200
201
|
completion:nil];
|
|
201
202
|
}
|
|
@@ -209,10 +210,26 @@ using namespace facebook::react;
|
|
|
209
210
|
options:curve | UIViewAnimationOptionBeginFromCurrentState
|
|
210
211
|
animations:^{
|
|
211
212
|
self.transform = CGAffineTransformIdentity;
|
|
213
|
+
[self syncEdgeEffect];
|
|
212
214
|
}
|
|
213
215
|
completion:nil];
|
|
214
216
|
}
|
|
215
217
|
|
|
218
|
+
// UIKit derives the edge effect region from model geometry at commit time,
|
|
219
|
+
// outside our animation context — the blur snaps to the footer's final
|
|
220
|
+
// position while the footer is still animating. Re-evaluating the scroll
|
|
221
|
+
// view's layout inside the animation block makes the region change inherit
|
|
222
|
+
// the keyboard curve.
|
|
223
|
+
- (void)syncEdgeEffect {
|
|
224
|
+
if (@available(iOS 26.0, *)) {
|
|
225
|
+
UIScrollView *scrollView = [self edgeInteractionScrollView];
|
|
226
|
+
if (scrollView) {
|
|
227
|
+
[scrollView setNeedsLayout];
|
|
228
|
+
[scrollView layoutIfNeeded];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
216
233
|
- (void)applyKeyboardOffset {
|
|
217
234
|
CGFloat height = self.keyboardObserver.currentHeight;
|
|
218
235
|
if (height <= 0 || !self.keyboardObserver.viewController.absoluteFooter) {
|
package/ios/TrueSheetView.mm
CHANGED
|
@@ -53,6 +53,7 @@ using namespace facebook::react;
|
|
|
53
53
|
NSInteger _initialDetentIndex;
|
|
54
54
|
TrueSheetViewInsetAdjustment _insetAdjustment;
|
|
55
55
|
ScrollableOptions *_scrollableOptions;
|
|
56
|
+
NSInteger _scrollableHandle;
|
|
56
57
|
BOOL _hasAutoDetent;
|
|
57
58
|
BOOL _initialDetentAnimated;
|
|
58
59
|
BOOL _isSheetUpdatePending;
|
|
@@ -277,17 +278,23 @@ using namespace facebook::react;
|
|
|
277
278
|
_initialDetentIndex = newProps.initialDetentIndex;
|
|
278
279
|
_initialDetentAnimated = newProps.initialDetentAnimated;
|
|
279
280
|
|
|
281
|
+
_scrollableHandle = newProps.scrollableHandle;
|
|
282
|
+
|
|
280
283
|
const auto &scrollableOpts = newProps.scrollableOptions;
|
|
281
284
|
BOOL scrollingExpandsSheet = scrollableOpts.scrollingExpandsSheet;
|
|
285
|
+
BOOL contentInsetAdjustment = scrollableOpts.contentInsetAdjustmentBehavior;
|
|
282
286
|
auto topEdgeEffect = scrollableOpts.topScrollEdgeEffect;
|
|
283
287
|
auto bottomEdgeEffect = scrollableOpts.bottomScrollEdgeEffect;
|
|
284
|
-
BOOL hasScrollableOptions = scrollableOpts.keyboardScrollOffset > 0 ||
|
|
288
|
+
BOOL hasScrollableOptions = scrollableOpts.keyboardScrollOffset > 0 || scrollableOpts.keyboardOffset != 0 ||
|
|
289
|
+
!scrollingExpandsSheet || !contentInsetAdjustment ||
|
|
285
290
|
topEdgeEffect != TrueSheetViewTopScrollEdgeEffect::Hidden ||
|
|
286
291
|
bottomEdgeEffect != TrueSheetViewBottomScrollEdgeEffect::Hidden;
|
|
287
292
|
|
|
288
293
|
if (hasScrollableOptions) {
|
|
289
294
|
ScrollableOptions *options = [[ScrollableOptions alloc] init];
|
|
295
|
+
options.contentInsetAdjustment = contentInsetAdjustment;
|
|
290
296
|
options.keyboardScrollOffset = scrollableOpts.keyboardScrollOffset;
|
|
297
|
+
options.keyboardOffset = scrollableOpts.keyboardOffset;
|
|
291
298
|
options.scrollingExpandsSheet = scrollingExpandsSheet;
|
|
292
299
|
options.topScrollEdgeEffect = topEdgeEffect;
|
|
293
300
|
options.bottomScrollEdgeEffect = bottomEdgeEffect;
|
|
@@ -502,7 +509,7 @@ using namespace facebook::react;
|
|
|
502
509
|
[_controller setupSheetSizing];
|
|
503
510
|
[_controller setupSheetProps];
|
|
504
511
|
|
|
505
|
-
//
|
|
512
|
+
// Detect before measuring so a first-time detection doesn't shift sizes mid-animation.
|
|
506
513
|
[self setupScrollable];
|
|
507
514
|
|
|
508
515
|
// Measure synchronously so the presentation starts at the right height —
|
|
@@ -671,7 +678,7 @@ using namespace facebook::react;
|
|
|
671
678
|
[self setupSheetDetentsForSizeChange];
|
|
672
679
|
}
|
|
673
680
|
|
|
674
|
-
// When the ScrollView changes (e.g. conditional remount), re-
|
|
681
|
+
// When the ScrollView changes (e.g. conditional remount), re-detect the new ScrollView.
|
|
675
682
|
- (void)containerViewScrollViewDidChange {
|
|
676
683
|
[self setupScrollable];
|
|
677
684
|
}
|
|
@@ -822,8 +829,10 @@ using namespace facebook::react;
|
|
|
822
829
|
if (!_containerView)
|
|
823
830
|
return;
|
|
824
831
|
|
|
825
|
-
_containerView.insetAdjustment = _insetAdjustment;
|
|
826
832
|
_containerView.scrollableOptions = _scrollableOptions;
|
|
833
|
+
_containerView.scrollableHandle = _scrollableHandle;
|
|
834
|
+
_containerView.contentInsetAdjustment = (_scrollableOptions ? _scrollableOptions.contentInsetAdjustment : YES) &&
|
|
835
|
+
_insetAdjustment == TrueSheetViewInsetAdjustment::Automatic;
|
|
827
836
|
_containerView.hasAutoDetent = _hasAutoDetent;
|
|
828
837
|
[self refreshFooterBottomInset];
|
|
829
838
|
[_containerView setupScrollable];
|
|
@@ -17,6 +17,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
17
17
|
- (void)setupEdgeInteractionWithScrollView:(nullable UIScrollView *)scrollView
|
|
18
18
|
edge:(UIRectEdge)edge API_AVAILABLE(ios(26.0));
|
|
19
19
|
- (void)cleanupEdgeInteraction API_AVAILABLE(ios(26.0));
|
|
20
|
+
- (nullable UIScrollView *)edgeInteractionScrollView API_AVAILABLE(ios(26.0));
|
|
20
21
|
|
|
21
22
|
@end
|
|
22
23
|
|
|
@@ -31,15 +31,37 @@ static const void *kEdgeEffectHintKey = &kEdgeEffectHintKey;
|
|
|
31
31
|
objc_setAssociatedObject(self, kEdgeEffectHintKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
- (nullable UIScrollView *)edgeInteractionScrollView API_AVAILABLE(ios(26.0)) {
|
|
35
|
+
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(26_0)
|
|
36
|
+
for (id<UIInteraction> interaction in self.interactions) {
|
|
37
|
+
if ([interaction isKindOfClass:[UIScrollEdgeElementContainerInteraction class]]) {
|
|
38
|
+
return ((UIScrollEdgeElementContainerInteraction *)interaction).scrollView;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
#endif
|
|
42
|
+
return nil;
|
|
43
|
+
}
|
|
44
|
+
|
|
34
45
|
- (void)setupEdgeInteractionWithScrollView:(nullable UIScrollView *)scrollView
|
|
35
46
|
edge:(UIRectEdge)edge API_AVAILABLE(ios(26.0)) {
|
|
36
|
-
[self cleanupEdgeInteraction];
|
|
37
|
-
|
|
38
47
|
if (!scrollView) {
|
|
48
|
+
[self cleanupEdgeInteraction];
|
|
39
49
|
return;
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(26_0)
|
|
53
|
+
// Update in place — this re-runs on every footer/header size change, and
|
|
54
|
+
// tearing the interaction down mid keyboard animation flashes the edge
|
|
55
|
+
// effect at the footer's final position.
|
|
56
|
+
for (id<UIInteraction> interaction in self.interactions) {
|
|
57
|
+
if ([interaction isKindOfClass:[UIScrollEdgeElementContainerInteraction class]]) {
|
|
58
|
+
UIScrollEdgeElementContainerInteraction *existing = (UIScrollEdgeElementContainerInteraction *)interaction;
|
|
59
|
+
existing.scrollView = scrollView;
|
|
60
|
+
existing.edge = edge;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
43
65
|
// UIScrollEdgeElementContainerInteraction requires standard UIKit element
|
|
44
66
|
// descendants (UILabel, UIControl, etc.) to trigger the edge effect.
|
|
45
67
|
// RCTViewComponentView subviews are not recognized, so we add a
|