@lodev09/react-native-true-sheet 3.11.10 → 4.0.0-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.
- package/README.md +6 -5
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +23 -2
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +116 -26
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +7 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +48 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterViewManager.kt +7 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +45 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +28 -12
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +99 -34
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +9 -5
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +12 -3
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +33 -4
- package/android/src/main/jni/TrueSheetSpec.h +2 -0
- package/android/src/main/jni/TrueSheetStateUpdater.cpp +96 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +24 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +43 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +28 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +11 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +37 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h +24 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +56 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +28 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +11 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +41 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.cpp +18 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.h +16 -0
- package/ios/TrueSheetContainerView.h +12 -10
- package/ios/TrueSheetContainerView.mm +11 -29
- package/ios/TrueSheetContentView.h +22 -7
- package/ios/TrueSheetContentView.mm +143 -41
- package/ios/TrueSheetFooterView.h +12 -2
- package/ios/TrueSheetFooterView.mm +74 -71
- package/ios/TrueSheetHeaderView.mm +0 -1
- package/ios/TrueSheetView.mm +60 -27
- package/ios/TrueSheetViewController.h +9 -0
- package/ios/TrueSheetViewController.mm +288 -122
- package/ios/core/TrueSheetDetentCalculator.h +9 -0
- package/ios/core/TrueSheetDetentCalculator.mm +13 -4
- package/ios/tvos/TrueSheetStubs.mm +2 -0
- package/lib/module/TrueSheet.js +32 -13
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +262 -38
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +5 -1
- package/lib/module/fabric/TrueSheetFooterViewNativeComponent.ts +10 -2
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +8 -1
- package/lib/module/web/vaul/index.js +17 -6
- package/lib/module/web/vaul/index.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +35 -11
- 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/TrueSheetContentViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts +2 -0
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +5 -1
- 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/lib/typescript/src/web/vaul/index.d.ts +8 -1
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
- package/package.json +10 -10
- package/react-native.config.js +5 -1
- package/src/TrueSheet.tsx +45 -14
- package/src/TrueSheet.types.ts +38 -12
- package/src/TrueSheet.web.tsx +314 -44
- package/src/fabric/TrueSheetContentViewNativeComponent.ts +5 -1
- package/src/fabric/TrueSheetFooterViewNativeComponent.ts +10 -2
- package/src/fabric/TrueSheetViewNativeComponent.ts +8 -1
- package/src/navigation/types.ts +3 -1
- package/src/web/vaul/index.tsx +23 -5
- package/ios/utils/LayoutUtil.h +0 -77
- package/ios/utils/LayoutUtil.mm +0 -106
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
|
|
11
11
|
#import "TrueSheetContentView.h"
|
|
12
12
|
#import <React/RCTScrollViewComponentView.h>
|
|
13
|
-
#import <react/renderer/components/TrueSheetSpec/ComponentDescriptors.h>
|
|
14
13
|
#import <react/renderer/components/TrueSheetSpec/EventEmitters.h>
|
|
15
14
|
#import <react/renderer/components/TrueSheetSpec/Props.h>
|
|
16
15
|
#import <react/renderer/components/TrueSheetSpec/RCTComponentViewHelpers.h>
|
|
16
|
+
#import <react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h>
|
|
17
|
+
#import <react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h>
|
|
17
18
|
#import "TrueSheetContainerView.h"
|
|
19
|
+
#import "TrueSheetFooterView.h"
|
|
18
20
|
#import "TrueSheetView.h"
|
|
19
21
|
#import "TrueSheetViewController.h"
|
|
20
22
|
#import "utils/PlatformUtil.h"
|
|
@@ -22,13 +24,18 @@
|
|
|
22
24
|
|
|
23
25
|
using namespace facebook::react;
|
|
24
26
|
|
|
27
|
+
static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
|
|
28
|
+
|
|
25
29
|
@implementation TrueSheetContentView {
|
|
30
|
+
TrueSheetContentViewShadowNode::ConcreteState::Shared _state;
|
|
26
31
|
RCTScrollViewComponentView *_pinnedScrollView;
|
|
32
|
+
UIScrollView *_observedScrollView;
|
|
27
33
|
CGSize _lastSize;
|
|
28
34
|
CGFloat _bottomInset;
|
|
29
|
-
CGFloat _originalScrollViewHeight;
|
|
30
35
|
CGFloat _originalIndicatorBottomInset;
|
|
31
36
|
BOOL _observingTextChanges;
|
|
37
|
+
BOOL _scrollableBounded;
|
|
38
|
+
BOOL _isReportPending;
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
@@ -45,6 +52,38 @@ using namespace facebook::react;
|
|
|
45
52
|
|
|
46
53
|
- (void)dealloc {
|
|
47
54
|
[self stopObservingTextChanges];
|
|
55
|
+
[self unobserveScrollViewContentSize];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState {
|
|
59
|
+
_state = std::static_pointer_cast<TrueSheetContentViewShadowNode::ConcreteState const>(state);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Tells the shadow node to fill the container (flexGrow/flexShrink) so the
|
|
63
|
+
// pinned ScrollView's viewport is bounded to the visible space. Only applied
|
|
64
|
+
// for auto detents — otherwise content lays out naturally like a regular view.
|
|
65
|
+
- (void)setScrollableBounded:(BOOL)bounded {
|
|
66
|
+
if (_scrollableBounded == bounded) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
_scrollableBounded = bounded;
|
|
70
|
+
|
|
71
|
+
if (_state) {
|
|
72
|
+
TrueSheetContentViewState newState;
|
|
73
|
+
newState.scrollableBounded = bounded;
|
|
74
|
+
_state->updateState(std::move(newState));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
- (void)setHasAutoDetent:(BOOL)hasAutoDetent {
|
|
79
|
+
if (_hasAutoDetent == hasAutoDetent) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
_hasAutoDetent = hasAutoDetent;
|
|
83
|
+
|
|
84
|
+
if (_pinnedScrollView) {
|
|
85
|
+
[self setScrollableBounded:hasAutoDetent];
|
|
86
|
+
}
|
|
48
87
|
}
|
|
49
88
|
|
|
50
89
|
#pragma mark - Text Change Observing
|
|
@@ -71,17 +110,61 @@ using namespace facebook::react;
|
|
|
71
110
|
|
|
72
111
|
#pragma mark - Layout
|
|
73
112
|
|
|
74
|
-
- (
|
|
75
|
-
|
|
76
|
-
|
|
113
|
+
- (CGFloat)naturalHeight {
|
|
114
|
+
CGFloat height = self.frame.size.height;
|
|
115
|
+
if (_pinnedScrollView) {
|
|
116
|
+
height += _pinnedScrollView.scrollView.contentSize.height - _pinnedScrollView.frame.size.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;
|
|
136
|
+
}
|
|
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
|
+
}
|
|
77
153
|
|
|
78
|
-
|
|
154
|
+
- (void)reportSizeNow {
|
|
155
|
+
CGSize newSize = CGSizeMake(self.frame.size.width, self.naturalHeight);
|
|
79
156
|
if (!CGSizeEqualToSize(newSize, _lastSize)) {
|
|
80
157
|
_lastSize = newSize;
|
|
81
158
|
[self.delegate contentViewDidChangeSize:newSize];
|
|
82
159
|
}
|
|
83
160
|
}
|
|
84
161
|
|
|
162
|
+
- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
|
|
163
|
+
oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics {
|
|
164
|
+
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
|
|
165
|
+
[self reportSizeIfChanged];
|
|
166
|
+
}
|
|
167
|
+
|
|
85
168
|
#pragma mark - Child Mounting
|
|
86
169
|
|
|
87
170
|
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
|
|
@@ -117,24 +200,17 @@ using namespace facebook::react;
|
|
|
117
200
|
|
|
118
201
|
- (void)clearScrollable {
|
|
119
202
|
if (_pinnedScrollView) {
|
|
120
|
-
CGRect frame = _pinnedScrollView.frame;
|
|
121
|
-
frame.size.height = _originalScrollViewHeight;
|
|
122
|
-
_pinnedScrollView.frame = frame;
|
|
123
|
-
|
|
124
203
|
[self setScrollViewContentInset:0 indicatorInset:_originalIndicatorBottomInset];
|
|
125
204
|
}
|
|
205
|
+
[self unobserveScrollViewContentSize];
|
|
206
|
+
[self setScrollableBounded:NO];
|
|
126
207
|
_pinnedScrollView = nil;
|
|
127
208
|
_bottomInset = 0;
|
|
128
|
-
_originalScrollViewHeight = 0;
|
|
129
209
|
_originalIndicatorBottomInset = 0;
|
|
210
|
+
[self reportSizeIfChanged];
|
|
130
211
|
}
|
|
131
212
|
|
|
132
|
-
- (void)
|
|
133
|
-
if (!enabled) {
|
|
134
|
-
[self clearScrollable];
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
213
|
+
- (void)setupScrollableWithBottomInset:(CGFloat)bottomInset {
|
|
138
214
|
// Check if pinned scroll view is still valid (still in view hierarchy)
|
|
139
215
|
if (_pinnedScrollView && ![_pinnedScrollView isDescendantOfView:self]) {
|
|
140
216
|
[self clearScrollable];
|
|
@@ -152,49 +228,71 @@ using namespace facebook::react;
|
|
|
152
228
|
|
|
153
229
|
// Only capture originals on first pin
|
|
154
230
|
if (!_pinnedScrollView) {
|
|
155
|
-
_originalScrollViewHeight = scrollView.frame.size.height;
|
|
156
231
|
_originalIndicatorBottomInset = scrollView.scrollView.verticalScrollIndicatorInsets.bottom;
|
|
157
232
|
_pinnedScrollView = scrollView;
|
|
233
|
+
|
|
234
|
+
[self observeScrollViewContentSize];
|
|
235
|
+
[self setScrollableBounded:_hasAutoDetent];
|
|
236
|
+
[self reportSizeIfChanged];
|
|
158
237
|
}
|
|
159
238
|
|
|
160
239
|
_bottomInset = bottomInset;
|
|
161
240
|
|
|
162
|
-
[self updateScrollViewHeight];
|
|
163
|
-
|
|
164
241
|
[self setScrollViewContentInset:_bottomInset indicatorInset:_originalIndicatorBottomInset];
|
|
165
242
|
|
|
166
243
|
// If keyboard is currently showing, re-apply the keyboard inset to the new ScrollView
|
|
167
244
|
CGFloat keyboardHeight = _keyboardObserver ? _keyboardObserver.currentHeight : 0;
|
|
168
245
|
if (keyboardHeight > 0) {
|
|
169
|
-
[self
|
|
246
|
+
CGFloat inset = [self keyboardInsetWithHeight:keyboardHeight];
|
|
247
|
+
[self setScrollViewContentInset:inset indicatorInset:_originalIndicatorBottomInset + inset];
|
|
170
248
|
}
|
|
171
249
|
}
|
|
172
250
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
+
// A relative footer stays behind the keyboard below the content, so its
|
|
254
|
+
// height shields that much of the keyboard's overlap.
|
|
255
|
+
- (CGFloat)keyboardInsetWithHeight:(CGFloat)height {
|
|
256
|
+
if (!self.footerView) {
|
|
257
|
+
return height;
|
|
176
258
|
}
|
|
259
|
+
if (_keyboardObserver.viewController.absoluteFooter) {
|
|
260
|
+
return height + [self.footerView keyboardOcclusionHeight];
|
|
261
|
+
}
|
|
262
|
+
return MAX(0, height - self.footerView.frame.size.height);
|
|
263
|
+
}
|
|
177
264
|
|
|
178
|
-
|
|
179
|
-
|
|
265
|
+
// Content growth is invisible to layout once the viewport is bounded, so track
|
|
266
|
+
// the scroll content size directly to keep the auto detent height in sync.
|
|
267
|
+
- (void)observeScrollViewContentSize {
|
|
268
|
+
UIScrollView *scrollView = _pinnedScrollView.scrollView;
|
|
269
|
+
if (_observedScrollView == scrollView) {
|
|
180
270
|
return;
|
|
181
271
|
}
|
|
272
|
+
[self unobserveScrollViewContentSize];
|
|
273
|
+
_observedScrollView = scrollView;
|
|
274
|
+
[scrollView addObserver:self
|
|
275
|
+
forKeyPath:@"contentSize"
|
|
276
|
+
options:NSKeyValueObservingOptionNew
|
|
277
|
+
context:TrueSheetContentSizeContext];
|
|
278
|
+
}
|
|
182
279
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
CGPoint savedContentOffset = _pinnedScrollView.scrollView.contentOffset;
|
|
190
|
-
|
|
191
|
-
CGRect frame = _pinnedScrollView.frame;
|
|
192
|
-
frame.size.height = newHeight;
|
|
193
|
-
_pinnedScrollView.frame = frame;
|
|
280
|
+
- (void)unobserveScrollViewContentSize {
|
|
281
|
+
if (_observedScrollView) {
|
|
282
|
+
[_observedScrollView removeObserver:self forKeyPath:@"contentSize" context:TrueSheetContentSizeContext];
|
|
283
|
+
_observedScrollView = nil;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
194
286
|
|
|
195
|
-
|
|
196
|
-
|
|
287
|
+
- (void)observeValueForKeyPath:(NSString *)keyPath
|
|
288
|
+
ofObject:(id)object
|
|
289
|
+
change:(NSDictionary *)change
|
|
290
|
+
context:(void *)context {
|
|
291
|
+
if (context == TrueSheetContentSizeContext) {
|
|
292
|
+
[self reportSizeIfChanged];
|
|
293
|
+
return;
|
|
197
294
|
}
|
|
295
|
+
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
|
198
296
|
}
|
|
199
297
|
|
|
200
298
|
- (RCTScrollViewComponentView *)findScrollView {
|
|
@@ -282,12 +380,12 @@ using namespace facebook::react;
|
|
|
282
380
|
TrueSheetViewController *sheetController = _keyboardObserver.viewController;
|
|
283
381
|
UIView *firstResponder = sheetController ? [sheetController.view findFirstResponder] : nil;
|
|
284
382
|
|
|
383
|
+
CGFloat inset = [self keyboardInsetWithHeight:height];
|
|
285
384
|
[UIView animateWithDuration:duration
|
|
286
385
|
delay:0
|
|
287
386
|
options:curve | UIViewAnimationOptionBeginFromCurrentState
|
|
288
387
|
animations:^{
|
|
289
|
-
[self setScrollViewContentInset:
|
|
290
|
-
indicatorInset:self->_originalIndicatorBottomInset + height];
|
|
388
|
+
[self setScrollViewContentInset:inset indicatorInset:self->_originalIndicatorBottomInset + inset];
|
|
291
389
|
}
|
|
292
390
|
completion:nil];
|
|
293
391
|
|
|
@@ -371,6 +469,10 @@ using namespace facebook::react;
|
|
|
371
469
|
[super prepareForRecycle];
|
|
372
470
|
[self stopObservingTextChanges];
|
|
373
471
|
[self clearScrollable];
|
|
472
|
+
_state.reset();
|
|
473
|
+
_scrollableBounded = NO;
|
|
474
|
+
_hasAutoDetent = NO;
|
|
475
|
+
_lastSize = CGSizeZero;
|
|
374
476
|
}
|
|
375
477
|
|
|
376
478
|
@end
|
|
@@ -27,14 +27,24 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
27
27
|
@property (nonatomic, weak, nullable) TrueSheetKeyboardObserver *keyboardObserver;
|
|
28
28
|
@property (nonatomic, weak, nullable) id<TrueSheetFooterViewDelegate> delegate;
|
|
29
29
|
|
|
30
|
-
- (void)setupConstraintsWithHeight:(CGFloat)height;
|
|
31
|
-
|
|
32
30
|
/**
|
|
33
31
|
* Re-applies the footer's keyboard slide using the current keyboard height.
|
|
34
32
|
* No-op when the keyboard is hidden. Used to reflect live `keyboardOffset` changes.
|
|
35
33
|
*/
|
|
36
34
|
- (void)applyKeyboardOffset;
|
|
37
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Bottom safe-area inset the footer absorbs as padding (via the shadow node)
|
|
38
|
+
* so its content clears the home indicator and the background fills the inset.
|
|
39
|
+
*/
|
|
40
|
+
- (void)setBottomInset:(CGFloat)bottomInset;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Height the footer occupies above the keyboard — its layout height minus
|
|
44
|
+
* the safe-area inset it drops while the keyboard is open.
|
|
45
|
+
*/
|
|
46
|
+
- (CGFloat)keyboardOcclusionHeight;
|
|
47
|
+
|
|
38
48
|
@end
|
|
39
49
|
|
|
40
50
|
NS_ASSUME_NONNULL_END
|
|
@@ -9,22 +9,22 @@
|
|
|
9
9
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
10
10
|
|
|
11
11
|
#import "TrueSheetFooterView.h"
|
|
12
|
-
#import <react/renderer/components/TrueSheetSpec/ComponentDescriptors.h>
|
|
13
12
|
#import <react/renderer/components/TrueSheetSpec/EventEmitters.h>
|
|
14
13
|
#import <react/renderer/components/TrueSheetSpec/Props.h>
|
|
15
14
|
#import <react/renderer/components/TrueSheetSpec/RCTComponentViewHelpers.h>
|
|
15
|
+
#import <react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h>
|
|
16
|
+
#import <react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h>
|
|
16
17
|
#import "TrueSheetViewController.h"
|
|
17
|
-
#import "utils/LayoutUtil.h"
|
|
18
18
|
#import "utils/UIView+ScrollEdgeInteraction.h"
|
|
19
19
|
|
|
20
20
|
using namespace facebook::react;
|
|
21
21
|
|
|
22
22
|
@implementation TrueSheetFooterView {
|
|
23
|
+
TrueSheetFooterViewShadowNode::ConcreteState::Shared _state;
|
|
23
24
|
CGFloat _lastHeight;
|
|
24
|
-
CGFloat _pendingHeight;
|
|
25
|
-
BOOL _didInitialLayout;
|
|
26
|
-
NSLayoutConstraint *_bottomConstraint;
|
|
27
25
|
CGFloat _currentKeyboardOffset;
|
|
26
|
+
CGFloat _bottomInset;
|
|
27
|
+
BOOL _keyboardVisible;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
@@ -40,14 +40,59 @@ using namespace facebook::react;
|
|
|
40
40
|
self.isAccessibilityElement = NO;
|
|
41
41
|
|
|
42
42
|
_lastHeight = 0;
|
|
43
|
-
_pendingHeight = 0;
|
|
44
|
-
_didInitialLayout = NO;
|
|
45
|
-
_bottomConstraint = nil;
|
|
46
43
|
_currentKeyboardOffset = 0;
|
|
44
|
+
_bottomInset = 0;
|
|
45
|
+
_keyboardVisible = NO;
|
|
47
46
|
}
|
|
48
47
|
return self;
|
|
49
48
|
}
|
|
50
49
|
|
|
50
|
+
- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState {
|
|
51
|
+
_state = std::static_pointer_cast<TrueSheetFooterViewShadowNode::ConcreteState const>(state);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Tells the shadow node to pad the footer's bottom edge with the sheet's
|
|
55
|
+
// bottom safe-area inset — the footer owns the sheet's bottom edge, so it
|
|
56
|
+
// absorbs the inset and its background fills it.
|
|
57
|
+
- (void)setBottomInset:(CGFloat)bottomInset {
|
|
58
|
+
if (_bottomInset == bottomInset) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
_bottomInset = bottomInset;
|
|
62
|
+
[self pushBottomInsetState];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Skipped while the keyboard is open — an absolute footer rises above the
|
|
66
|
+
// keyboard, so the inset would leave a gap.
|
|
67
|
+
- (void)setKeyboardVisible:(BOOL)keyboardVisible {
|
|
68
|
+
if (_keyboardVisible == keyboardVisible) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
_keyboardVisible = keyboardVisible;
|
|
72
|
+
[self pushBottomInsetState];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
- (void)pushBottomInsetState {
|
|
76
|
+
if (!_state) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
TrueSheetFooterViewState newState;
|
|
81
|
+
newState.bottomInset = _keyboardVisible ? 0 : _bottomInset;
|
|
82
|
+
newState.initialized = true;
|
|
83
|
+
// Immediate so the inset lands before anything reads the footer's height —
|
|
84
|
+
// the async path races the keyboard caret scroll and detent setup, leaving
|
|
85
|
+
// them an inset stale (same reason Android bridges through
|
|
86
|
+
// TrueSheetStateUpdater).
|
|
87
|
+
_state->updateState(std::move(newState), EventQueue::UpdateMode::unstable_Immediate);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Height the footer occupies above the keyboard — its layout height minus the
|
|
91
|
+
// safe-area inset it drops while the keyboard is open.
|
|
92
|
+
- (CGFloat)keyboardOcclusionHeight {
|
|
93
|
+
return MAX(0, _lastHeight - (_keyboardVisible ? 0 : _bottomInset));
|
|
94
|
+
}
|
|
95
|
+
|
|
51
96
|
#pragma mark - Accessibility
|
|
52
97
|
|
|
53
98
|
- (NSArray *)accessibilityElements {
|
|
@@ -74,57 +119,13 @@ using namespace facebook::react;
|
|
|
74
119
|
|
|
75
120
|
#pragma mark - Layout
|
|
76
121
|
|
|
77
|
-
- (void)setupConstraintsWithHeight:(CGFloat)height {
|
|
78
|
-
UIView *parentView = self.superview;
|
|
79
|
-
if (!parentView) {
|
|
80
|
-
// On recycled views, updateLayoutMetrics can fire before the view is
|
|
81
|
-
// reparented. Remember the desired height so didMoveToSuperview applies
|
|
82
|
-
// it instead of falling back to the stale self.frame from the previous
|
|
83
|
-
// present cycle.
|
|
84
|
-
_pendingHeight = height;
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
[LayoutUtil unpinView:self fromParentView:parentView];
|
|
89
|
-
_bottomConstraint = nil;
|
|
90
|
-
|
|
91
|
-
self.translatesAutoresizingMaskIntoConstraints = NO;
|
|
92
|
-
|
|
93
|
-
[self.leadingAnchor constraintEqualToAnchor:parentView.leadingAnchor].active = YES;
|
|
94
|
-
[self.trailingAnchor constraintEqualToAnchor:parentView.trailingAnchor].active = YES;
|
|
95
|
-
|
|
96
|
-
_bottomConstraint = [self.bottomAnchor constraintEqualToAnchor:parentView.bottomAnchor
|
|
97
|
-
constant:-_currentKeyboardOffset];
|
|
98
|
-
_bottomConstraint.active = YES;
|
|
99
|
-
|
|
100
|
-
if (height > 0) {
|
|
101
|
-
[self.heightAnchor constraintEqualToConstant:height].active = YES;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
_lastHeight = height;
|
|
105
|
-
_pendingHeight = 0;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
- (void)didMoveToSuperview {
|
|
109
|
-
[super didMoveToSuperview];
|
|
110
|
-
|
|
111
|
-
if (self.superview) {
|
|
112
|
-
CGFloat initialHeight = _pendingHeight > 0 ? _pendingHeight : self.frame.size.height;
|
|
113
|
-
[self setupConstraintsWithHeight:initialHeight];
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
122
|
- (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics
|
|
118
123
|
oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (!_didInitialLayout) {
|
|
122
|
-
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
|
|
123
|
-
_didInitialLayout = YES;
|
|
124
|
-
}
|
|
124
|
+
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
|
|
125
125
|
|
|
126
|
+
CGFloat height = layoutMetrics.frame.size.height;
|
|
126
127
|
if (height != _lastHeight) {
|
|
127
|
-
|
|
128
|
+
_lastHeight = height;
|
|
128
129
|
[self.delegate footerViewDidChangeSize:CGSizeMake(layoutMetrics.frame.size.width, height)];
|
|
129
130
|
}
|
|
130
131
|
}
|
|
@@ -132,25 +133,31 @@ using namespace facebook::react;
|
|
|
132
133
|
- (void)prepareForRecycle {
|
|
133
134
|
[super prepareForRecycle];
|
|
134
135
|
|
|
135
|
-
[LayoutUtil unpinView:self fromParentView:self.superview];
|
|
136
136
|
if (@available(iOS 26.0, *)) {
|
|
137
137
|
[self cleanupEdgeInteraction];
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
self.transform = CGAffineTransformIdentity;
|
|
141
|
+
_state = nullptr;
|
|
140
142
|
_lastHeight = 0;
|
|
141
|
-
_pendingHeight = 0;
|
|
142
|
-
_didInitialLayout = NO;
|
|
143
|
-
_bottomConstraint = nil;
|
|
144
143
|
_currentKeyboardOffset = 0;
|
|
144
|
+
_bottomInset = 0;
|
|
145
|
+
_keyboardVisible = NO;
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
#pragma mark - TrueSheetKeyboardObserverDelegate
|
|
148
149
|
|
|
150
|
+
// Yoga owns the footer's frame (pinned to the container's bottom edge), so the
|
|
151
|
+
// keyboard slide is carried by a transform instead of layout. Only an absolute
|
|
152
|
+
// footer floats above the keyboard — a relative footer stays in the layout
|
|
153
|
+
// flow behind it.
|
|
149
154
|
- (void)keyboardWillShow:(CGFloat)height duration:(NSTimeInterval)duration curve:(UIViewAnimationOptions)curve {
|
|
150
|
-
if (!
|
|
155
|
+
if (!self.keyboardObserver.viewController.absoluteFooter) {
|
|
151
156
|
return;
|
|
152
157
|
}
|
|
153
158
|
|
|
159
|
+
[self setKeyboardVisible:YES];
|
|
160
|
+
|
|
154
161
|
CGFloat keyboardOffset = self.keyboardObserver.viewController.footerKeyboardOffset;
|
|
155
162
|
CGFloat slide = MAX(0, height + keyboardOffset);
|
|
156
163
|
_currentKeyboardOffset = slide;
|
|
@@ -159,40 +166,36 @@ using namespace facebook::react;
|
|
|
159
166
|
delay:0
|
|
160
167
|
options:curve | UIViewAnimationOptionBeginFromCurrentState
|
|
161
168
|
animations:^{
|
|
162
|
-
self
|
|
163
|
-
[self.superview layoutIfNeeded];
|
|
169
|
+
self.transform = CGAffineTransformMakeTranslation(0, -slide);
|
|
164
170
|
}
|
|
165
171
|
completion:nil];
|
|
166
172
|
}
|
|
167
173
|
|
|
168
174
|
- (void)keyboardWillHide:(NSTimeInterval)duration curve:(UIViewAnimationOptions)curve {
|
|
169
|
-
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
|
|
175
|
+
[self setKeyboardVisible:NO];
|
|
173
176
|
_currentKeyboardOffset = 0;
|
|
174
177
|
|
|
175
178
|
[UIView animateWithDuration:duration
|
|
176
179
|
delay:0
|
|
177
180
|
options:curve | UIViewAnimationOptionBeginFromCurrentState
|
|
178
181
|
animations:^{
|
|
179
|
-
self
|
|
180
|
-
[self.superview layoutIfNeeded];
|
|
182
|
+
self.transform = CGAffineTransformIdentity;
|
|
181
183
|
}
|
|
182
184
|
completion:nil];
|
|
183
185
|
}
|
|
184
186
|
|
|
185
187
|
- (void)applyKeyboardOffset {
|
|
186
188
|
CGFloat height = self.keyboardObserver.currentHeight;
|
|
187
|
-
if (
|
|
189
|
+
if (height <= 0 || !self.keyboardObserver.viewController.absoluteFooter) {
|
|
188
190
|
return;
|
|
189
191
|
}
|
|
190
192
|
|
|
193
|
+
[self setKeyboardVisible:YES];
|
|
194
|
+
|
|
191
195
|
CGFloat keyboardOffset = self.keyboardObserver.viewController.footerKeyboardOffset;
|
|
192
196
|
CGFloat slide = MAX(0, height + keyboardOffset);
|
|
193
197
|
_currentKeyboardOffset = slide;
|
|
194
|
-
|
|
195
|
-
[self.superview layoutIfNeeded];
|
|
198
|
+
self.transform = CGAffineTransformMakeTranslation(0, -slide);
|
|
196
199
|
}
|
|
197
200
|
|
|
198
201
|
@end
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
#import <react/renderer/components/TrueSheetSpec/EventEmitters.h>
|
|
14
14
|
#import <react/renderer/components/TrueSheetSpec/Props.h>
|
|
15
15
|
#import <react/renderer/components/TrueSheetSpec/RCTComponentViewHelpers.h>
|
|
16
|
-
#import "utils/LayoutUtil.h"
|
|
17
16
|
#import "utils/UIView+ScrollEdgeInteraction.h"
|
|
18
17
|
|
|
19
18
|
using namespace facebook::react;
|