@lodev09/react-native-true-sheet 4.0.0-beta.14 → 4.0.0-beta.15
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 -6
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +99 -7
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +12 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +18 -7
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +40 -39
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +18 -8
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +14 -8
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +8 -1
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +32 -5
- package/ios/TrueSheetContainerView.h +17 -4
- package/ios/TrueSheetContainerView.mm +23 -6
- package/ios/TrueSheetContentView.h +13 -1
- package/ios/TrueSheetContentView.mm +93 -35
- package/ios/TrueSheetFooterView.mm +19 -5
- package/ios/TrueSheetView.mm +29 -28
- package/ios/TrueSheetViewController.h +21 -1
- package/ios/TrueSheetViewController.mm +54 -16
- package/ios/core/TrueSheetDetentCalculator.h +6 -1
- package/ios/core/TrueSheetDetentCalculator.mm +9 -4
- package/ios/core/TrueSheetKeyboardObserver.mm +2 -0
- package/lib/module/TrueSheet.js +8 -5
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +135 -32
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/TrueSheetPeek.web.js +20 -15
- package/lib/module/TrueSheetPeek.web.js.map +1 -1
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +5 -3
- package/lib/module/navigation/expo-router/index.js +5 -4
- package/lib/module/navigation/expo-router/index.js.map +1 -1
- package/lib/module/web/constants.js +1 -1
- package/lib/module/web/constants.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +64 -25
- 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/TrueSheetPeek.web.d.ts +8 -4
- package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +5 -3
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/navigation/expo-router/index.d.ts +5 -3
- package/lib/typescript/src/navigation/expo-router/index.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/constants.d.ts +1 -1
- package/lib/typescript/src/web/constants.d.ts.map +1 -1
- package/package.json +7 -6
- package/src/TrueSheet.tsx +7 -4
- package/src/TrueSheet.types.ts +67 -25
- package/src/TrueSheet.web.tsx +154 -30
- package/src/TrueSheetPeek.web.tsx +25 -23
- package/src/fabric/TrueSheetViewNativeComponent.ts +5 -3
- package/src/navigation/expo-router/index.ts +8 -11
- package/src/navigation/types.ts +3 -2
- package/src/web/constants.ts +1 -1
|
@@ -72,6 +72,7 @@ class TrueSheetCoordinatorLayout(context: Context) :
|
|
|
72
72
|
// manually.
|
|
73
73
|
private var streamSheetDraggable = false
|
|
74
74
|
private var streamDraggableEditText = false
|
|
75
|
+
private var streamScrollsIndependently = false
|
|
75
76
|
|
|
76
77
|
// Pull-down tracking. BottomSheetBehavior gives no callback when a downward drag
|
|
77
78
|
// is clamped by `isHideable = false` — the sheet stays put (or the scrollable
|
|
@@ -196,6 +197,7 @@ class TrueSheetCoordinatorLayout(context: Context) :
|
|
|
196
197
|
private fun updateStreamTargetFlags(ev: MotionEvent) {
|
|
197
198
|
streamSheetDraggable = false
|
|
198
199
|
streamDraggableEditText = false
|
|
200
|
+
streamScrollsIndependently = false
|
|
199
201
|
|
|
200
202
|
val sheet = delegate?.findSheetView() ?: return
|
|
201
203
|
val target = TouchTargetHelper.findTargetPathAndCoordinatesForTouch(ev.x, ev.y, this, FloatArray(2))
|
|
@@ -215,7 +217,10 @@ class TrueSheetCoordinatorLayout(context: Context) :
|
|
|
215
217
|
|
|
216
218
|
var view: View? = target
|
|
217
219
|
while (view != null && view !== this) {
|
|
218
|
-
if (view.canScrollVertically(1) || view.canScrollVertically(-1))
|
|
220
|
+
if (view.canScrollVertically(1) || view.canScrollVertically(-1)) {
|
|
221
|
+
streamScrollsIndependently = !view.isNestedScrollingEnabled
|
|
222
|
+
return
|
|
223
|
+
}
|
|
219
224
|
view = view.parent as? View
|
|
220
225
|
}
|
|
221
226
|
|
|
@@ -297,6 +302,8 @@ class TrueSheetCoordinatorLayout(context: Context) :
|
|
|
297
302
|
return false
|
|
298
303
|
}
|
|
299
304
|
|
|
305
|
+
if (streamScrollsIndependently) return false
|
|
306
|
+
|
|
300
307
|
if (streamGestureClaimed) {
|
|
301
308
|
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
|
302
309
|
streamGestureClaimed = false
|
|
@@ -18,6 +18,15 @@ interface TrueSheetDetentCalculatorDelegate {
|
|
|
18
18
|
val absoluteHeader: Boolean
|
|
19
19
|
val footerHeight: Int
|
|
20
20
|
val absoluteFooter: Boolean
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* True while the plugged scrollable is padded by the absolute footer's
|
|
24
|
+
* height (see ScrollableOptions.footerInsetAdjustment) — the footer then
|
|
25
|
+
* counts toward the auto detent like a relative one, so the content ends
|
|
26
|
+
* above it.
|
|
27
|
+
*/
|
|
28
|
+
val footerInsetAdjustment: Boolean
|
|
29
|
+
val footerAvoidsKeyboard: Boolean
|
|
21
30
|
val peekContentHeight: Int
|
|
22
31
|
val contentBottomInset: Int
|
|
23
32
|
val maxContentHeight: Int?
|
|
@@ -53,6 +62,14 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
|
|
|
53
62
|
private val topInset: Int get() = delegate?.topInset ?: 0
|
|
54
63
|
private val hasKeyboardFloor: Boolean get() = delegate?.hasKeyboardFloor == true
|
|
55
64
|
|
|
65
|
+
/**
|
|
66
|
+
* An absolute footer floats over the content and contributes no height to
|
|
67
|
+
* the auto detent — unless it pads the scrollable, then the content ends
|
|
68
|
+
* above it like a relative footer.
|
|
69
|
+
*/
|
|
70
|
+
private val footerInAuto: Boolean
|
|
71
|
+
get() = delegate?.absoluteFooter == false || delegate?.footerInsetAdjustment == true
|
|
72
|
+
|
|
56
73
|
/**
|
|
57
74
|
* Height for auto (-1.0) detents: content + header + footer height.
|
|
58
75
|
* An absolute (floating) header or footer overlaps the content, so it contributes no height.
|
|
@@ -60,14 +77,14 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
|
|
|
60
77
|
private val autoDetentHeight: Int
|
|
61
78
|
get() = contentHeight +
|
|
62
79
|
(if (delegate?.absoluteHeader == true) 0 else headerHeight) +
|
|
63
|
-
(if (
|
|
80
|
+
(if (footerInAuto) footerHeight else 0)
|
|
64
81
|
|
|
65
82
|
/**
|
|
66
|
-
* Bottom inset for auto detents. A
|
|
67
|
-
* edge, so it absorbs the inset instead of adding it
|
|
83
|
+
* Bottom inset for auto detents. A footer counted in the auto height owns
|
|
84
|
+
* the sheet's bottom edge, so it absorbs the inset instead of adding it.
|
|
68
85
|
*/
|
|
69
86
|
private val autoDetentBottomInset: Int
|
|
70
|
-
get() = if (footerHeight > 0 &&
|
|
87
|
+
get() = if (footerHeight > 0 && footerInAuto) 0 else contentBottomInset
|
|
71
88
|
|
|
72
89
|
/**
|
|
73
90
|
* Bottom inset for peek detents. An absolute footer counts toward the peek
|
|
@@ -105,7 +122,17 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
|
|
|
105
122
|
(detent * screenHeight).toInt() + contentBottomInset
|
|
106
123
|
}
|
|
107
124
|
|
|
108
|
-
val
|
|
125
|
+
val keyboardHeight = if (includeKeyboard) keyboardInset else 0
|
|
126
|
+
val footerKeyboardOverlap = if (detent == -1.0 &&
|
|
127
|
+
delegate?.absoluteFooter == true &&
|
|
128
|
+
footerInAuto &&
|
|
129
|
+
delegate?.footerAvoidsKeyboard == false
|
|
130
|
+
) {
|
|
131
|
+
minOf(keyboardHeight, maxOf(0, footerHeight - contentBottomInset))
|
|
132
|
+
} else {
|
|
133
|
+
0
|
|
134
|
+
}
|
|
135
|
+
val height = baseHeight + keyboardHeight - footerKeyboardOverlap
|
|
109
136
|
val maxAllowedHeight = screenHeight + contentBottomInset
|
|
110
137
|
return maxContentHeight?.let { minOf(height, it, maxAllowedHeight) } ?: minOf(height, maxAllowedHeight)
|
|
111
138
|
}
|
|
@@ -16,13 +16,19 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
16
16
|
|
|
17
17
|
@interface ScrollableOptions : NSObject
|
|
18
18
|
|
|
19
|
-
@property (nonatomic, assign)
|
|
19
|
+
@property (nonatomic, assign) facebook::react::TrueSheetViewContentInsetAdjustment contentInsetAdjustment;
|
|
20
20
|
@property (nonatomic, assign) CGFloat keyboardScrollOffset;
|
|
21
21
|
@property (nonatomic, assign) CGFloat keyboardOffset;
|
|
22
22
|
@property (nonatomic, assign) BOOL scrollingExpandsSheet;
|
|
23
23
|
@property (nonatomic, assign) facebook::react::TrueSheetViewTopScrollEdgeEffect topScrollEdgeEffect;
|
|
24
24
|
@property (nonatomic, assign) facebook::react::TrueSheetViewBottomScrollEdgeEffect bottomScrollEdgeEffect;
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Derived from `contentInsetAdjustment` — which of the two bottom insets apply
|
|
28
|
+
*/
|
|
29
|
+
@property (nonatomic, readonly) BOOL safeAreaInsetAdjustment;
|
|
30
|
+
@property (nonatomic, readonly) BOOL footerInsetAdjustment;
|
|
31
|
+
|
|
26
32
|
@end
|
|
27
33
|
|
|
28
34
|
@class TrueSheetPeekView;
|
|
@@ -58,10 +64,17 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
58
64
|
@property (nonatomic, assign) NSInteger scrollableHandle;
|
|
59
65
|
|
|
60
66
|
/**
|
|
61
|
-
* Whether the resolved scrollable
|
|
62
|
-
* `automatic` — already gated by the
|
|
67
|
+
* Whether the resolved scrollable gets the bottom safe-area inset (UIKit's
|
|
68
|
+
* `contentInsetAdjustmentBehavior` set to `automatic`) — already gated by the
|
|
69
|
+
* sheet's `insetAdjustment`
|
|
70
|
+
*/
|
|
71
|
+
@property (nonatomic, assign) BOOL safeAreaInsetAdjustment;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether the resolved scrollable is padded by the absolute footer's height —
|
|
75
|
+
* already gated by the footer position and a plugged scrollable
|
|
63
76
|
*/
|
|
64
|
-
@property (nonatomic, assign) BOOL
|
|
77
|
+
@property (nonatomic, assign) BOOL footerInsetAdjustment;
|
|
65
78
|
|
|
66
79
|
/**
|
|
67
80
|
* Bottom safe-area inset the footer absorbs as padding — the footer
|
|
@@ -32,7 +32,7 @@ using namespace facebook::react;
|
|
|
32
32
|
|
|
33
33
|
- (instancetype)init {
|
|
34
34
|
if (self = [super init]) {
|
|
35
|
-
_contentInsetAdjustment =
|
|
35
|
+
_contentInsetAdjustment = TrueSheetViewContentInsetAdjustment::Automatic;
|
|
36
36
|
_keyboardScrollOffset = 0;
|
|
37
37
|
_keyboardOffset = 0;
|
|
38
38
|
_scrollingExpandsSheet = YES;
|
|
@@ -42,6 +42,16 @@ using namespace facebook::react;
|
|
|
42
42
|
return self;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
- (BOOL)safeAreaInsetAdjustment {
|
|
46
|
+
return _contentInsetAdjustment == TrueSheetViewContentInsetAdjustment::Automatic ||
|
|
47
|
+
_contentInsetAdjustment == TrueSheetViewContentInsetAdjustment::SafeArea;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
- (BOOL)footerInsetAdjustment {
|
|
51
|
+
return _contentInsetAdjustment == TrueSheetViewContentInsetAdjustment::Automatic ||
|
|
52
|
+
_contentInsetAdjustment == TrueSheetViewContentInsetAdjustment::Footer;
|
|
53
|
+
}
|
|
54
|
+
|
|
45
55
|
@end
|
|
46
56
|
|
|
47
57
|
@interface TrueSheetContainerView () <TrueSheetContentViewDelegate,
|
|
@@ -153,10 +163,16 @@ using namespace facebook::react;
|
|
|
153
163
|
_contentView.keyboardOffset = scrollableOptions ? scrollableOptions.keyboardOffset : 0;
|
|
154
164
|
}
|
|
155
165
|
|
|
166
|
+
- (void)setFooterInsetAdjustment:(BOOL)footerInsetAdjustment {
|
|
167
|
+
_footerInsetAdjustment = footerInsetAdjustment;
|
|
168
|
+
_contentView.footerInsetAdjustment = footerInsetAdjustment;
|
|
169
|
+
}
|
|
170
|
+
|
|
156
171
|
- (void)setupScrollable {
|
|
157
172
|
if (_contentView) {
|
|
158
173
|
_contentView.scrollableHandle = _scrollableHandle;
|
|
159
|
-
_contentView.
|
|
174
|
+
_contentView.safeAreaInsetAdjustment = _safeAreaInsetAdjustment;
|
|
175
|
+
_contentView.footerInsetAdjustment = _footerInsetAdjustment;
|
|
160
176
|
[_contentView setupScrollable];
|
|
161
177
|
[_contentView applyScrollEdgeEffects:_scrollableOptions];
|
|
162
178
|
if (@available(iOS 26.0, *)) {
|
|
@@ -202,6 +218,7 @@ using namespace facebook::react;
|
|
|
202
218
|
_contentView = (TrueSheetContentView *)childComponentView;
|
|
203
219
|
_contentView.delegate = self;
|
|
204
220
|
_contentView.footerView = _footerView;
|
|
221
|
+
_contentView.footerInsetAdjustment = _footerInsetAdjustment;
|
|
205
222
|
|
|
206
223
|
// Children mount bottom-up, so the content subtree is complete here.
|
|
207
224
|
// Late-mounted peek views attach themselves instead (see TrueSheetPeekView).
|
|
@@ -283,6 +300,7 @@ using namespace facebook::react;
|
|
|
283
300
|
|
|
284
301
|
- (void)footerViewDidChangeSize:(CGSize)newSize {
|
|
285
302
|
[self.delegate containerViewFooterDidChangeSize:newSize];
|
|
303
|
+
[_contentView updateBottomInset];
|
|
286
304
|
if (@available(iOS 26.0, *)) {
|
|
287
305
|
[self setupEdgeInteractions];
|
|
288
306
|
}
|
|
@@ -310,10 +328,9 @@ using namespace facebook::react;
|
|
|
310
328
|
return;
|
|
311
329
|
}
|
|
312
330
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
331
|
+
// Last-attached wins. During a screen transition the incoming peek mounts
|
|
332
|
+
// before the outgoing one detaches, and the outgoing detach is a no-op.
|
|
333
|
+
_peekView.delegate = nil;
|
|
317
334
|
|
|
318
335
|
_peekView = peekView;
|
|
319
336
|
peekView.delegate = self;
|
|
@@ -69,10 +69,22 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
69
69
|
* `automatic` while plugged so UIKit applies the bottom safe-area inset to
|
|
70
70
|
* the scroll content. The previous behavior is restored on clear.
|
|
71
71
|
*/
|
|
72
|
-
@property (nonatomic, assign) BOOL
|
|
72
|
+
@property (nonatomic, assign) BOOL safeAreaInsetAdjustment;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Pads the resolved ScrollView's bottom content inset by the absolute
|
|
76
|
+
* footer's height so the last item scrolls into view above it. Stacks with
|
|
77
|
+
* the keyboard inset.
|
|
78
|
+
*/
|
|
79
|
+
@property (nonatomic, assign) BOOL footerInsetAdjustment;
|
|
73
80
|
|
|
74
81
|
- (RCTScrollViewComponentView *_Nullable)findScrollView;
|
|
75
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Re-applies the footer and keyboard insets — call when the footer's size changes
|
|
85
|
+
*/
|
|
86
|
+
- (void)updateBottomInset;
|
|
87
|
+
|
|
76
88
|
/**
|
|
77
89
|
* Resolve the ScrollView from `scrollableHandle`, wiring keyboard handling
|
|
78
90
|
*/
|
|
@@ -30,7 +30,7 @@ using namespace facebook::react;
|
|
|
30
30
|
CGFloat _appliedKeyboardOffset;
|
|
31
31
|
BOOL _observingTextChanges;
|
|
32
32
|
UIScrollViewContentInsetAdjustmentBehavior _originalInsetAdjustmentBehavior;
|
|
33
|
-
BOOL
|
|
33
|
+
BOOL _appliedSafeAreaInsetAdjustment;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
@@ -69,29 +69,38 @@ using namespace facebook::react;
|
|
|
69
69
|
[self clearScrollable];
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
- (void)
|
|
73
|
-
if (
|
|
72
|
+
- (void)setSafeAreaInsetAdjustment:(BOOL)safeAreaInsetAdjustment {
|
|
73
|
+
if (_safeAreaInsetAdjustment == safeAreaInsetAdjustment) {
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
|
-
|
|
77
|
-
[self
|
|
76
|
+
_safeAreaInsetAdjustment = safeAreaInsetAdjustment;
|
|
77
|
+
[self applySafeAreaInsetAdjustment];
|
|
78
|
+
[self updateBottomInset];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
- (void)setFooterInsetAdjustment:(BOOL)footerInsetAdjustment {
|
|
82
|
+
if (_footerInsetAdjustment == footerInsetAdjustment) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
_footerInsetAdjustment = footerInsetAdjustment;
|
|
86
|
+
[self updateBottomInset];
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
// Hands the bottom safe-area inset to UIKit — `automatic` only insets while
|
|
81
90
|
// the content can scroll, which the Android counterpart mirrors manually.
|
|
82
|
-
- (void)
|
|
91
|
+
- (void)applySafeAreaInsetAdjustment {
|
|
83
92
|
if (!_detectedScrollView) {
|
|
84
93
|
return;
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
UIScrollView *scrollView = _detectedScrollView.scrollView;
|
|
88
|
-
if (
|
|
97
|
+
if (_safeAreaInsetAdjustment && !_appliedSafeAreaInsetAdjustment) {
|
|
89
98
|
_originalInsetAdjustmentBehavior = scrollView.contentInsetAdjustmentBehavior;
|
|
90
99
|
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAutomatic;
|
|
91
|
-
|
|
92
|
-
} else if (!
|
|
100
|
+
_appliedSafeAreaInsetAdjustment = YES;
|
|
101
|
+
} else if (!_safeAreaInsetAdjustment && _appliedSafeAreaInsetAdjustment) {
|
|
93
102
|
scrollView.contentInsetAdjustmentBehavior = _originalInsetAdjustmentBehavior;
|
|
94
|
-
|
|
103
|
+
_appliedSafeAreaInsetAdjustment = NO;
|
|
95
104
|
}
|
|
96
105
|
}
|
|
97
106
|
|
|
@@ -169,14 +178,16 @@ using namespace facebook::react;
|
|
|
169
178
|
[self.delegate contentViewScrollViewDidChange];
|
|
170
179
|
}
|
|
171
180
|
|
|
172
|
-
|
|
181
|
+
// Layout has settled for the whole tree — the footer overlap (sheet height,
|
|
182
|
+
// scroll view frame) and the keyboard inset are current here
|
|
183
|
+
[self updateBottomInset];
|
|
173
184
|
}
|
|
174
185
|
|
|
175
186
|
#pragma mark - Scrollable
|
|
176
187
|
|
|
177
188
|
// The scroll indicator follows automatically — UIKit derives its insets from
|
|
178
189
|
// the content inset while `automaticallyAdjustsScrollIndicatorInsets` is set.
|
|
179
|
-
- (void)
|
|
190
|
+
- (void)setBottomInset:(CGFloat)inset {
|
|
180
191
|
if (!_detectedScrollView)
|
|
181
192
|
return;
|
|
182
193
|
|
|
@@ -189,19 +200,57 @@ using namespace facebook::react;
|
|
|
189
200
|
_detectedScrollView.scrollView.contentInset = contentInset;
|
|
190
201
|
}
|
|
191
202
|
|
|
192
|
-
|
|
203
|
+
// How much of the absolute footer covers the scroll view — measured from the
|
|
204
|
+
// footer's layout position (pinned to the container's bottom edge; its
|
|
205
|
+
// keyboard rise is a transform) so a short scroll view ending above the footer
|
|
206
|
+
// gets no inset. UIKit stacks the safe area on top of contentInset while the
|
|
207
|
+
// adjustment behavior is automatic — take out the inset baked into the
|
|
208
|
+
// footer's height so the total lands on the footer's top edge. Stays constant
|
|
209
|
+
// across keyboard transitions: the footer drops its inset as it rises, and
|
|
210
|
+
// the baked value drops with it. A pinned footer adds only the overlap not
|
|
211
|
+
// already covered by the keyboard inset.
|
|
212
|
+
- (CGFloat)footerInsetAbove:(CGFloat)keyboardInset {
|
|
213
|
+
if (!_footerInsetAdjustment || !self.footerView || !_detectedScrollView) {
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
UIView *container = self.footerView.superview;
|
|
218
|
+
UIScrollView *scrollView = _detectedScrollView.scrollView;
|
|
219
|
+
CGRect scrollFrame = [scrollView convertRect:scrollView.bounds toView:container];
|
|
220
|
+
CGFloat footerTop = container.bounds.size.height - self.footerView.bounds.size.height;
|
|
221
|
+
|
|
222
|
+
CGFloat inset = CGRectGetMaxY(scrollFrame) - footerTop;
|
|
223
|
+
if (_appliedSafeAreaInsetAdjustment) {
|
|
224
|
+
inset -= self.footerView.appliedBottomInset;
|
|
225
|
+
}
|
|
226
|
+
if (!_keyboardObserver.viewController.footerAvoidsKeyboard) {
|
|
227
|
+
inset -= keyboardInset;
|
|
228
|
+
}
|
|
229
|
+
return MAX(0, inset);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
- (CGFloat)keyboardInset {
|
|
193
233
|
CGFloat keyboardHeight = _keyboardObserver.currentHeight;
|
|
194
|
-
|
|
195
|
-
|
|
234
|
+
return keyboardHeight > 0 ? [self keyboardInsetWithHeight:keyboardHeight] : 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Safe to call mid keyboard animation: the target matches the animation's
|
|
238
|
+
// model value (the footer inset doesn't change with the keyboard), so the
|
|
239
|
+
// early-out in setBottomInset: leaves the animation alone.
|
|
240
|
+
- (void)updateBottomInset {
|
|
241
|
+
if (!_detectedScrollView) {
|
|
242
|
+
return;
|
|
196
243
|
}
|
|
244
|
+
CGFloat keyboardInset = [self keyboardInset];
|
|
245
|
+
[self setBottomInset:[self footerInsetAbove:keyboardInset] + keyboardInset];
|
|
197
246
|
}
|
|
198
247
|
|
|
199
248
|
- (void)clearScrollable {
|
|
200
|
-
[self
|
|
249
|
+
[self setBottomInset:0];
|
|
201
250
|
_appliedKeyboardOffset = 0;
|
|
202
|
-
if (
|
|
251
|
+
if (_appliedSafeAreaInsetAdjustment) {
|
|
203
252
|
_detectedScrollView.scrollView.contentInsetAdjustmentBehavior = _originalInsetAdjustmentBehavior;
|
|
204
|
-
|
|
253
|
+
_appliedSafeAreaInsetAdjustment = NO;
|
|
205
254
|
}
|
|
206
255
|
_detectedScrollView = nil;
|
|
207
256
|
}
|
|
@@ -223,10 +272,10 @@ using namespace facebook::react;
|
|
|
223
272
|
|
|
224
273
|
_detectedScrollView = scrollView;
|
|
225
274
|
|
|
226
|
-
[self
|
|
275
|
+
[self applySafeAreaInsetAdjustment];
|
|
227
276
|
|
|
228
|
-
//
|
|
229
|
-
[self
|
|
277
|
+
// Apply the footer inset, and the keyboard inset if it's currently showing
|
|
278
|
+
[self updateBottomInset];
|
|
230
279
|
}
|
|
231
280
|
|
|
232
281
|
// Short or nested scroll views can end above the sheet's bottom edge.
|
|
@@ -240,18 +289,21 @@ using namespace facebook::react;
|
|
|
240
289
|
|
|
241
290
|
// UIKit stacks the safe area on top of contentInset while the adjustment
|
|
242
291
|
// behavior is automatic — take it out so the total lands on the keyboard edge.
|
|
243
|
-
if (
|
|
292
|
+
if (_appliedSafeAreaInsetAdjustment) {
|
|
244
293
|
inset = MAX(0, inset - scrollView.safeAreaInsets.bottom);
|
|
245
294
|
}
|
|
246
295
|
|
|
247
296
|
// Track how much of keyboardOffset actually lands in the inset so the caret
|
|
248
297
|
// reveal can compensate — the offset shifts the inset, not the keyboard edge.
|
|
249
298
|
CGFloat adjustedInset = MAX(0, inset + self.keyboardOffset);
|
|
250
|
-
_appliedKeyboardOffset =
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
//
|
|
254
|
-
|
|
299
|
+
_appliedKeyboardOffset =
|
|
300
|
+
adjustedInset + [self footerInsetAbove:adjustedInset] - (inset + [self footerInsetAbove:inset]);
|
|
301
|
+
|
|
302
|
+
// Content that already fits above the keyboard (and footer) has nothing to
|
|
303
|
+
// reveal — the inset would only open blank scroll range below it (huge gap
|
|
304
|
+
// at the bottom).
|
|
305
|
+
if (scrollView.contentSize.height + [self footerInsetAbove:adjustedInset] <=
|
|
306
|
+
scrollView.bounds.size.height - adjustedInset) {
|
|
255
307
|
_appliedKeyboardOffset = 0;
|
|
256
308
|
return 0;
|
|
257
309
|
}
|
|
@@ -260,9 +312,13 @@ using namespace facebook::react;
|
|
|
260
312
|
}
|
|
261
313
|
|
|
262
314
|
// An absolute footer floats over the viewport's bottom edge — extend the
|
|
263
|
-
// caret target so it clears the footer, not just the keyboard.
|
|
315
|
+
// caret target so it clears the footer, not just the keyboard. Not needed
|
|
316
|
+
// while the footer inset is applied (the visible rect already ends above it)
|
|
317
|
+
// or when the footer stays behind the keyboard.
|
|
264
318
|
- (CGFloat)footerOcclusion {
|
|
265
|
-
|
|
319
|
+
TrueSheetViewController *controller = _keyboardObserver.viewController;
|
|
320
|
+
if (self.footerView && controller.absoluteFooter && controller.footerAvoidsKeyboard &&
|
|
321
|
+
[self footerInsetAbove:0] <= 0) {
|
|
266
322
|
return [self.footerView keyboardOcclusionHeight];
|
|
267
323
|
}
|
|
268
324
|
return 0;
|
|
@@ -340,15 +396,16 @@ using namespace facebook::react;
|
|
|
340
396
|
TrueSheetViewController *sheetController = _keyboardObserver.viewController;
|
|
341
397
|
UIView *firstResponder = sheetController ? [sheetController.view findFirstResponder] : nil;
|
|
342
398
|
|
|
343
|
-
CGFloat
|
|
399
|
+
CGFloat keyboardInset = [self keyboardInsetWithHeight:height];
|
|
400
|
+
CGFloat inset = [self footerInsetAbove:keyboardInset] + keyboardInset;
|
|
344
401
|
[UIView animateWithDuration:duration
|
|
345
402
|
delay:0
|
|
346
403
|
options:curve | UIViewAnimationOptionBeginFromCurrentState
|
|
347
404
|
animations:^{
|
|
348
|
-
[self
|
|
405
|
+
[self setBottomInset:inset];
|
|
349
406
|
}
|
|
350
407
|
completion:^(BOOL finished) {
|
|
351
|
-
[self
|
|
408
|
+
[self updateBottomInset];
|
|
352
409
|
}];
|
|
353
410
|
|
|
354
411
|
// Defer scroll until the next run loop so content insets are applied first
|
|
@@ -373,7 +430,7 @@ using namespace facebook::react;
|
|
|
373
430
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
374
431
|
// Typing can grow the content (e.g. a multiline input) past the fits-above-
|
|
375
432
|
// the-keyboard threshold — keep the inset in sync before revealing the caret.
|
|
376
|
-
[self
|
|
433
|
+
[self updateBottomInset];
|
|
377
434
|
[self scrollToFocusedCaretAnimated:YES];
|
|
378
435
|
});
|
|
379
436
|
}
|
|
@@ -422,7 +479,7 @@ using namespace facebook::react;
|
|
|
422
479
|
delay:0
|
|
423
480
|
options:curve | UIViewAnimationOptionBeginFromCurrentState
|
|
424
481
|
animations:^{
|
|
425
|
-
[self
|
|
482
|
+
[self setBottomInset:[self footerInsetAbove:0]];
|
|
426
483
|
}
|
|
427
484
|
completion:nil];
|
|
428
485
|
}
|
|
@@ -435,7 +492,8 @@ using namespace facebook::react;
|
|
|
435
492
|
[self clearScrollable];
|
|
436
493
|
_state.reset();
|
|
437
494
|
_scrollableHandle = 0;
|
|
438
|
-
|
|
495
|
+
_safeAreaInsetAdjustment = NO;
|
|
496
|
+
_footerInsetAdjustment = NO;
|
|
439
497
|
_lastReportedNaturalHeight = 0;
|
|
440
498
|
}
|
|
441
499
|
|
|
@@ -176,12 +176,18 @@ using namespace facebook::react;
|
|
|
176
176
|
|
|
177
177
|
#pragma mark - TrueSheetKeyboardObserverDelegate
|
|
178
178
|
|
|
179
|
+
// Only an absolute footer floats above the keyboard — a relative footer stays
|
|
180
|
+
// in the layout flow behind it, and an absolute one opts out via
|
|
181
|
+
// footerOptions.avoidKeyboard.
|
|
182
|
+
- (BOOL)floatsAboveKeyboard {
|
|
183
|
+
TrueSheetViewController *controller = self.keyboardObserver.viewController;
|
|
184
|
+
return controller.absoluteFooter && controller.footerAvoidsKeyboard;
|
|
185
|
+
}
|
|
186
|
+
|
|
179
187
|
// Yoga owns the footer's frame (pinned to the container's bottom edge), so the
|
|
180
|
-
// keyboard slide is carried by a transform instead of layout.
|
|
181
|
-
// footer floats above the keyboard — a relative footer stays in the layout
|
|
182
|
-
// flow behind it.
|
|
188
|
+
// keyboard slide is carried by a transform instead of layout.
|
|
183
189
|
- (void)keyboardWillShow:(CGFloat)height duration:(NSTimeInterval)duration curve:(UIViewAnimationOptions)curve {
|
|
184
|
-
if (!self
|
|
190
|
+
if (![self floatsAboveKeyboard]) {
|
|
185
191
|
return;
|
|
186
192
|
}
|
|
187
193
|
|
|
@@ -232,7 +238,15 @@ using namespace facebook::react;
|
|
|
232
238
|
|
|
233
239
|
- (void)applyKeyboardOffset {
|
|
234
240
|
CGFloat height = self.keyboardObserver.currentHeight;
|
|
235
|
-
if (height <= 0
|
|
241
|
+
if (height <= 0) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// avoidKeyboard flipped off mid-keyboard — drop the slide already applied
|
|
246
|
+
if (![self floatsAboveKeyboard]) {
|
|
247
|
+
[self setKeyboardVisible:NO];
|
|
248
|
+
_currentKeyboardOffset = 0;
|
|
249
|
+
self.transform = CGAffineTransformIdentity;
|
|
236
250
|
return;
|
|
237
251
|
}
|
|
238
252
|
|
package/ios/TrueSheetView.mm
CHANGED
|
@@ -244,6 +244,8 @@ using namespace facebook::react;
|
|
|
244
244
|
_controller.blurIntensity = blurOpts.intensity >= 0 ? @(blurOpts.intensity) : nil;
|
|
245
245
|
_controller.blurInteraction = blurOpts.interaction;
|
|
246
246
|
|
|
247
|
+
_controller.glass = newProps.glass;
|
|
248
|
+
|
|
247
249
|
// Corner radius
|
|
248
250
|
_controller.cornerRadius = newProps.cornerRadius < 0 ? nil : @(newProps.cornerRadius);
|
|
249
251
|
|
|
@@ -253,8 +255,7 @@ using namespace facebook::react;
|
|
|
253
255
|
// Content width
|
|
254
256
|
_controller.maxContentWidth = newProps.maxContentWidth != 0.0 ? @(newProps.maxContentWidth) : nil;
|
|
255
257
|
|
|
256
|
-
|
|
257
|
-
_controller.anchor = newProps.anchor;
|
|
258
|
+
_controller.placement = newProps.placement;
|
|
258
259
|
|
|
259
260
|
_controller.grabber = newProps.grabber;
|
|
260
261
|
|
|
@@ -300,36 +301,35 @@ using namespace facebook::react;
|
|
|
300
301
|
_scrollableHandle = newProps.scrollableHandle;
|
|
301
302
|
|
|
302
303
|
const auto &scrollableOpts = newProps.scrollableOptions;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
_scrollableOptions = options;
|
|
321
|
-
} else {
|
|
322
|
-
_scrollableOptions = nil;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
_controller.scrollingExpandsSheet = scrollingExpandsSheet;
|
|
304
|
+
ScrollableOptions *options = [[ScrollableOptions alloc] init];
|
|
305
|
+
options.contentInsetAdjustment = scrollableOpts.contentInsetAdjustment;
|
|
306
|
+
options.keyboardScrollOffset = scrollableOpts.keyboardScrollOffset;
|
|
307
|
+
options.keyboardOffset = scrollableOpts.keyboardOffset;
|
|
308
|
+
options.scrollingExpandsSheet = scrollableOpts.scrollingExpandsSheet;
|
|
309
|
+
options.topScrollEdgeEffect = scrollableOpts.topScrollEdgeEffect;
|
|
310
|
+
options.bottomScrollEdgeEffect = scrollableOpts.bottomScrollEdgeEffect;
|
|
311
|
+
|
|
312
|
+
// nil while every option is at its default
|
|
313
|
+
BOOL hasScrollableOptions = options.keyboardScrollOffset > 0 || options.keyboardOffset != 0 ||
|
|
314
|
+
!options.scrollingExpandsSheet ||
|
|
315
|
+
options.contentInsetAdjustment != TrueSheetViewContentInsetAdjustment::Automatic ||
|
|
316
|
+
options.topScrollEdgeEffect != TrueSheetViewTopScrollEdgeEffect::Hidden ||
|
|
317
|
+
options.bottomScrollEdgeEffect != TrueSheetViewBottomScrollEdgeEffect::Hidden;
|
|
318
|
+
_scrollableOptions = hasScrollableOptions ? options : nil;
|
|
319
|
+
|
|
320
|
+
_controller.scrollingExpandsSheet = options.scrollingExpandsSheet;
|
|
326
321
|
|
|
327
322
|
_controller.absoluteHeader = newProps.headerOptions.position == TrueSheetViewPosition::Absolute;
|
|
328
323
|
_controller.absoluteFooter = newProps.footerOptions.footerPosition == TrueSheetViewFooterPosition::Absolute;
|
|
324
|
+
_controller.footerInsetAdjustment =
|
|
325
|
+
options.footerInsetAdjustment && _controller.absoluteFooter && _scrollableHandle > 0;
|
|
329
326
|
|
|
330
327
|
CGFloat footerKeyboardOffset = newProps.footerOptions.keyboardOffset;
|
|
331
|
-
|
|
328
|
+
BOOL footerAvoidsKeyboard = newProps.footerOptions.avoidKeyboard;
|
|
329
|
+
if (_controller.footerKeyboardOffset != footerKeyboardOffset ||
|
|
330
|
+
_controller.footerAvoidsKeyboard != footerAvoidsKeyboard) {
|
|
332
331
|
_controller.footerKeyboardOffset = footerKeyboardOffset;
|
|
332
|
+
_controller.footerAvoidsKeyboard = footerAvoidsKeyboard;
|
|
333
333
|
[_containerView updateFooterKeyboardOffset];
|
|
334
334
|
}
|
|
335
335
|
|
|
@@ -900,8 +900,9 @@ using namespace facebook::react;
|
|
|
900
900
|
|
|
901
901
|
_containerView.scrollableOptions = _scrollableOptions;
|
|
902
902
|
_containerView.scrollableHandle = _scrollableHandle;
|
|
903
|
-
_containerView.
|
|
904
|
-
|
|
903
|
+
_containerView.safeAreaInsetAdjustment = (_scrollableOptions ? _scrollableOptions.safeAreaInsetAdjustment : YES) &&
|
|
904
|
+
_insetAdjustment == TrueSheetViewInsetAdjustment::Automatic;
|
|
905
|
+
_containerView.footerInsetAdjustment = _controller.footerInsetAdjustment;
|
|
905
906
|
[self refreshFooterBottomInset];
|
|
906
907
|
[_containerView setupScrollable];
|
|
907
908
|
}
|
|
@@ -62,6 +62,13 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
62
62
|
@property (nonatomic, assign) BOOL absoluteHeader;
|
|
63
63
|
@property (nonatomic, strong, nullable) NSNumber *footerHeight;
|
|
64
64
|
@property (nonatomic, assign) BOOL absoluteFooter;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* YES while the plugged scrollable is padded by the absolute footer's height
|
|
68
|
+
* (see ScrollableOptions.contentInsetAdjustment) — the footer then counts
|
|
69
|
+
* toward the auto detent like a relative one, so the content ends above it.
|
|
70
|
+
*/
|
|
71
|
+
@property (nonatomic, assign) BOOL footerInsetAdjustment;
|
|
65
72
|
@property (nonatomic, strong, nullable) NSNumber *peekContentHeight;
|
|
66
73
|
@property (nonatomic, strong, nullable) UIColor *backgroundColor;
|
|
67
74
|
@property (nonatomic, strong, nullable) NSNumber *cornerRadius;
|
|
@@ -74,11 +81,18 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
74
81
|
@property (nonatomic, assign) facebook::react::TrueSheetViewBackgroundBlur backgroundBlur;
|
|
75
82
|
@property (nonatomic, strong, nullable) NSNumber *blurIntensity;
|
|
76
83
|
@property (nonatomic, assign) BOOL blurInteraction;
|
|
84
|
+
@property (nonatomic, assign) BOOL glass;
|
|
77
85
|
@property (nonatomic, assign) facebook::react::TrueSheetViewPresentation presentation;
|
|
78
|
-
@property (nonatomic, assign) facebook::react::
|
|
86
|
+
@property (nonatomic, assign) facebook::react::TrueSheetViewPlacement placement;
|
|
79
87
|
@property (nonatomic, assign) facebook::react::TrueSheetViewInsetAdjustment insetAdjustment;
|
|
80
88
|
@property (nonatomic, assign) BOOL scrollingExpandsSheet;
|
|
81
89
|
@property (nonatomic, assign) CGFloat footerKeyboardOffset;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* NO keeps an absolute footer pinned to the sheet's bottom edge behind the
|
|
93
|
+
* keyboard instead of rising above it.
|
|
94
|
+
*/
|
|
95
|
+
@property (nonatomic, assign) BOOL footerAvoidsKeyboard;
|
|
82
96
|
@property (nonatomic, assign) BOOL dismissible;
|
|
83
97
|
@property (nonatomic, assign) BOOL isPresented;
|
|
84
98
|
@property (nonatomic, assign) NSInteger activeDetentIndex;
|
|
@@ -89,6 +103,12 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
89
103
|
* ~keyboardHeight into the learned offset.
|
|
90
104
|
*/
|
|
91
105
|
@property (nonatomic, assign) BOOL keyboardSheetGrown;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Target keyboard height, set as soon as the keyboard frame change is
|
|
109
|
+
* announced — ahead of the grow/shrink animation, unlike keyboardSheetGrown.
|
|
110
|
+
*/
|
|
111
|
+
@property (nonatomic, assign) CGFloat keyboardHeight;
|
|
92
112
|
@property (nonatomic, readonly) BOOL isTopmostPresentedController;
|
|
93
113
|
@property (nonatomic, readonly) BOOL isStackedBehindChild;
|
|
94
114
|
@property (nonatomic, weak, nullable) UIView *accessibilityContentView;
|