@lodev09/react-native-true-sheet 3.11.3 → 3.11.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/RNTrueSheet.podspec +6 -2
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +62 -4
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +2 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +5 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +22 -1
- package/ios/TrueSheetContainerView.h +5 -0
- package/ios/TrueSheetContainerView.mm +6 -0
- package/ios/TrueSheetContentView.mm +82 -4
- package/ios/TrueSheetView.mm +7 -0
- package/ios/TrueSheetViewController.h +1 -0
- package/ios/TrueSheetViewController.mm +9 -0
- package/ios/core/TrueSheetDetentCalculator.h +9 -0
- package/ios/core/TrueSheetDetentCalculator.mm +8 -0
- package/ios/tvos/TrueSheetStubs.mm +141 -0
- package/lib/module/TrueSheet.js +2 -1
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +24 -4
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/web/vaul/constants.js +1 -0
- package/lib/module/web/vaul/constants.js.map +1 -1
- package/lib/module/web/vaul/index.js +2 -0
- package/lib/module/web/vaul/index.js.map +1 -1
- package/lib/module/web/vaul/use-snap-points.js +6 -4
- package/lib/module/web/vaul/use-snap-points.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +8 -0
- 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/web/vaul/constants.d.ts +1 -0
- package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/index.d.ts +6 -1
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts +2 -1
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/TrueSheet.tsx +2 -1
- package/src/TrueSheet.types.ts +9 -0
- package/src/TrueSheet.web.tsx +32 -7
- package/src/web/vaul/constants.ts +2 -0
- package/src/web/vaul/index.tsx +7 -0
- package/src/web/vaul/use-snap-points.ts +17 -4
package/RNTrueSheet.podspec
CHANGED
|
@@ -10,10 +10,14 @@ Pod::Spec.new do |s|
|
|
|
10
10
|
s.license = package["license"]
|
|
11
11
|
s.authors = package["author"]
|
|
12
12
|
|
|
13
|
-
s.platforms = { :ios => min_ios_version_supported }
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported, :tvos => min_ios_version_supported }
|
|
14
14
|
s.source = { :git => "https://github.com/lodev09/react-native-true-sheet.git", :tag => "#{s.version}" }
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
# tvOS compiles stubs only — sheets are not supported there, but codegen
|
|
17
|
+
# still registers the components so the classes must exist (#722)
|
|
18
|
+
s.ios.source_files = "ios/**/*.{h,m,mm,swift,cpp}", "common/cpp/**/*.{cpp,h}"
|
|
19
|
+
s.ios.exclude_files = "ios/tvos/**/*"
|
|
20
|
+
s.tvos.source_files = "ios/tvos/**/*.mm", "common/cpp/**/*.{cpp,h}"
|
|
17
21
|
s.private_header_files = "ios/**/*.h"
|
|
18
22
|
|
|
19
23
|
s.pod_target_xcconfig = {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
package com.lodev09.truesheet
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
|
+
import android.text.Editable
|
|
5
|
+
import android.text.TextWatcher
|
|
4
6
|
import android.view.View
|
|
5
7
|
import android.view.ViewGroup
|
|
8
|
+
import android.widget.EditText
|
|
6
9
|
import android.widget.ScrollView
|
|
7
10
|
import androidx.core.widget.NestedScrollView
|
|
8
11
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
@@ -45,6 +48,9 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
45
48
|
private var keyboardScrollOffset: Float = 0f
|
|
46
49
|
private var keyboardObserver: TrueSheetKeyboardObserver? = null
|
|
47
50
|
|
|
51
|
+
private var watchedEditText: EditText? = null
|
|
52
|
+
private var caretTextWatcher: TextWatcher? = null
|
|
53
|
+
|
|
48
54
|
var scrollableOptions: ScrollableOptions? = null
|
|
49
55
|
set(value) {
|
|
50
56
|
field = value
|
|
@@ -190,14 +196,17 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
190
196
|
}
|
|
191
197
|
|
|
192
198
|
override fun keyboardDidShow(height: Int) {
|
|
199
|
+
attachCaretListener()
|
|
193
200
|
scrollToFocusedInput()
|
|
194
201
|
}
|
|
195
202
|
|
|
196
203
|
override fun keyboardWillHide() {
|
|
204
|
+
detachCaretListener()
|
|
197
205
|
updateScrollViewInsetForKeyboard(0)
|
|
198
206
|
}
|
|
199
207
|
|
|
200
208
|
override fun focusDidChange(newFocus: View) {
|
|
209
|
+
attachCaretListener()
|
|
201
210
|
scrollToFocusedInput()
|
|
202
211
|
}
|
|
203
212
|
}
|
|
@@ -206,10 +215,37 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
206
215
|
}
|
|
207
216
|
|
|
208
217
|
fun cleanupKeyboardHandler() {
|
|
218
|
+
detachCaretListener()
|
|
209
219
|
keyboardObserver?.stop()
|
|
210
220
|
keyboardObserver = null
|
|
211
221
|
}
|
|
212
222
|
|
|
223
|
+
private fun attachCaretListener() {
|
|
224
|
+
val focused = findFocus() as? EditText
|
|
225
|
+
if (focused === watchedEditText) return
|
|
226
|
+
|
|
227
|
+
detachCaretListener()
|
|
228
|
+
if (focused == null) return
|
|
229
|
+
|
|
230
|
+
val watcher = object : TextWatcher {
|
|
231
|
+
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
|
232
|
+
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
|
233
|
+
override fun afterTextChanged(s: Editable?) {
|
|
234
|
+
focused.post { scrollToFocusedInput() }
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
focused.addTextChangedListener(watcher)
|
|
239
|
+
watchedEditText = focused
|
|
240
|
+
caretTextWatcher = watcher
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private fun detachCaretListener() {
|
|
244
|
+
caretTextWatcher?.let { watchedEditText?.removeTextChangedListener(it) }
|
|
245
|
+
caretTextWatcher = null
|
|
246
|
+
watchedEditText = null
|
|
247
|
+
}
|
|
248
|
+
|
|
213
249
|
private fun updateScrollViewInsetForKeyboard(keyboardHeight: Int) {
|
|
214
250
|
val scrollView = pinnedScrollView ?: return
|
|
215
251
|
|
|
@@ -234,14 +270,36 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
234
270
|
focusedView.getLocationOnScreen(focusedLocation)
|
|
235
271
|
scrollView.getLocationOnScreen(scrollViewLocation)
|
|
236
272
|
|
|
237
|
-
val
|
|
238
|
-
|
|
273
|
+
val viewTop = focusedLocation[1] - scrollViewLocation[1] + scrollView.scrollY
|
|
274
|
+
|
|
275
|
+
// Resolve the caret's line within the focused input so we follow the cursor
|
|
276
|
+
// rather than the input's full bounds (which over-scrolls tall inputs).
|
|
277
|
+
val editText = focusedView as? EditText
|
|
278
|
+
val layout = editText?.layout
|
|
279
|
+
|
|
280
|
+
val caretTop: Int
|
|
281
|
+
val caretBottom: Int
|
|
282
|
+
if (editText != null && layout != null) {
|
|
283
|
+
val line = layout.getLineForOffset(editText.selectionEnd.coerceAtLeast(0))
|
|
284
|
+
val textTop = viewTop + editText.totalPaddingTop - editText.scrollY
|
|
285
|
+
caretTop = textTop + layout.getLineTop(line)
|
|
286
|
+
caretBottom = textTop + layout.getLineBottom(line)
|
|
287
|
+
} else {
|
|
288
|
+
caretTop = viewTop
|
|
289
|
+
caretBottom = viewTop + focusedView.height
|
|
290
|
+
}
|
|
239
291
|
|
|
292
|
+
val offset = keyboardScrollOffset.toInt()
|
|
240
293
|
val visibleHeight = scrollView.height - scrollView.paddingBottom
|
|
294
|
+
val visibleTop = scrollView.scrollY
|
|
241
295
|
val visibleBottom = scrollView.scrollY + visibleHeight
|
|
242
296
|
|
|
243
|
-
|
|
244
|
-
|
|
297
|
+
when {
|
|
298
|
+
caretBottom + offset > visibleBottom ->
|
|
299
|
+
scrollView.smoothScrollTo(0, caretBottom + offset - visibleHeight)
|
|
300
|
+
|
|
301
|
+
caretTop - offset < visibleTop ->
|
|
302
|
+
scrollView.smoothScrollTo(0, (caretTop - offset).coerceAtLeast(0))
|
|
245
303
|
}
|
|
246
304
|
}
|
|
247
305
|
|
|
@@ -615,7 +615,8 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
615
615
|
}
|
|
616
616
|
|
|
617
617
|
override fun containerViewFooterDidChangeSize(width: Int, height: Int) {
|
|
618
|
-
// Footer
|
|
618
|
+
// Footer height affects peek detents
|
|
619
|
+
updateSheetIfNeeded()
|
|
619
620
|
viewController.positionFooter()
|
|
620
621
|
}
|
|
621
622
|
|
|
@@ -275,6 +275,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
275
275
|
// Cached values used during dismiss when container is unmounted
|
|
276
276
|
private var cachedContentHeight: Int = 0
|
|
277
277
|
private var cachedHeaderHeight: Int = 0
|
|
278
|
+
private var cachedFooterHeight: Int = 0
|
|
278
279
|
|
|
279
280
|
override val contentHeight: Int
|
|
280
281
|
get() = containerView?.contentHeight ?: cachedContentHeight
|
|
@@ -282,6 +283,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
282
283
|
override val headerHeight: Int
|
|
283
284
|
get() = containerView?.headerHeight ?: cachedHeaderHeight
|
|
284
285
|
|
|
286
|
+
override val footerHeight: Int
|
|
287
|
+
get() = containerView?.footerHeight ?: cachedFooterHeight
|
|
288
|
+
|
|
285
289
|
// Insets
|
|
286
290
|
// Target keyboard height used for detent calculations
|
|
287
291
|
override val keyboardInset: Int
|
|
@@ -836,6 +840,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
836
840
|
containerView?.let {
|
|
837
841
|
cachedContentHeight = it.contentHeight
|
|
838
842
|
cachedHeaderHeight = it.headerHeight
|
|
843
|
+
cachedFooterHeight = it.footerHeight
|
|
839
844
|
}
|
|
840
845
|
|
|
841
846
|
interactionState = InteractionState.Reconfiguring
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.lodev09.truesheet.core
|
|
2
2
|
|
|
3
|
+
import com.facebook.react.uimanager.PixelUtil.dpToPx
|
|
3
4
|
import com.facebook.react.uimanager.PixelUtil.pxToDp
|
|
4
5
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
5
6
|
import com.facebook.react.util.RNLog
|
|
@@ -14,6 +15,7 @@ interface TrueSheetDetentCalculatorDelegate {
|
|
|
14
15
|
val detents: MutableList<Double>
|
|
15
16
|
val contentHeight: Int
|
|
16
17
|
val headerHeight: Int
|
|
18
|
+
val footerHeight: Int
|
|
17
19
|
val contentBottomInset: Int
|
|
18
20
|
val maxContentHeight: Int?
|
|
19
21
|
val keyboardInset: Int
|
|
@@ -31,17 +33,30 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
|
|
|
31
33
|
private val detents: List<Double> get() = delegate?.detents ?: emptyList()
|
|
32
34
|
private val contentHeight: Int get() = delegate?.contentHeight ?: 0
|
|
33
35
|
private val headerHeight: Int get() = delegate?.headerHeight ?: 0
|
|
36
|
+
private val footerHeight: Int get() = delegate?.footerHeight ?: 0
|
|
34
37
|
private val contentBottomInset: Int get() = delegate?.contentBottomInset ?: 0
|
|
35
38
|
private val maxContentHeight: Int? get() = delegate?.maxContentHeight
|
|
36
39
|
private val keyboardInset: Int get() = delegate?.keyboardInset ?: 0
|
|
37
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Height for peek (-2.0) detents: header + footer height.
|
|
43
|
+
* Falls back to 150dp when neither is present.
|
|
44
|
+
*/
|
|
45
|
+
private val peekDetentHeight: Int
|
|
46
|
+
get() {
|
|
47
|
+
val height = headerHeight + footerHeight
|
|
48
|
+
return if (height > 0) height else DEFAULT_PEEK_HEIGHT.dpToPx().toInt()
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
/**
|
|
39
52
|
* Calculate the height in pixels for a given detent value.
|
|
40
|
-
* @param detent The detent value: -1.0 for content-fit, or 0.0-1.0 for screen fraction
|
|
53
|
+
* @param detent The detent value: -1.0 for content-fit, -2.0 for peek, or 0.0-1.0 for screen fraction
|
|
41
54
|
*/
|
|
42
55
|
fun getDetentHeight(detent: Double, includeKeyboard: Boolean = true): Int {
|
|
43
56
|
val baseHeight = if (detent == -1.0) {
|
|
44
57
|
contentHeight + headerHeight + contentBottomInset
|
|
58
|
+
} else if (detent == -2.0) {
|
|
59
|
+
peekDetentHeight + contentBottomInset
|
|
45
60
|
} else {
|
|
46
61
|
if (detent <= 0.0 || detent > 1.0) {
|
|
47
62
|
throw IllegalArgumentException("TrueSheet: detent fraction ($detent) must be between 0 and 1")
|
|
@@ -83,6 +98,8 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
|
|
|
83
98
|
val value = detents[index]
|
|
84
99
|
return if (value == -1.0) {
|
|
85
100
|
(contentHeight + headerHeight).toFloat() / screenHeight.toFloat()
|
|
101
|
+
} else if (value == -2.0) {
|
|
102
|
+
peekDetentHeight.toFloat() / screenHeight.toFloat()
|
|
86
103
|
} else {
|
|
87
104
|
value.toFloat()
|
|
88
105
|
}
|
|
@@ -211,4 +228,8 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
|
|
|
211
228
|
val toDetent = getDetentValueForIndex(toIndex)
|
|
212
229
|
return fromDetent + progress * (toDetent - fromDetent)
|
|
213
230
|
}
|
|
231
|
+
|
|
232
|
+
companion object {
|
|
233
|
+
private const val DEFAULT_PEEK_HEIGHT = 150f
|
|
234
|
+
}
|
|
214
235
|
}
|
|
@@ -119,6 +119,10 @@ using namespace facebook::react;
|
|
|
119
119
|
return _headerView ? _headerView.frame.size.height : 0;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
- (CGFloat)footerHeight {
|
|
123
|
+
return _footerView ? _footerView.frame.size.height : 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
122
126
|
- (void)layoutFooter {
|
|
123
127
|
if (_footerView) {
|
|
124
128
|
CGFloat height = _footerView.frame.size.height;
|
|
@@ -211,6 +215,7 @@ using namespace facebook::react;
|
|
|
211
215
|
}
|
|
212
216
|
_footerView = (TrueSheetFooterView *)childComponentView;
|
|
213
217
|
_footerView.delegate = self;
|
|
218
|
+
[self footerViewDidChangeSize:_footerView.frame.size];
|
|
214
219
|
}
|
|
215
220
|
}
|
|
216
221
|
|
|
@@ -229,6 +234,7 @@ using namespace facebook::react;
|
|
|
229
234
|
if ([childComponentView isKindOfClass:[TrueSheetFooterView class]]) {
|
|
230
235
|
_footerView.delegate = nil;
|
|
231
236
|
_footerView = nil;
|
|
237
|
+
[self footerViewDidChangeSize:CGSizeZero];
|
|
232
238
|
}
|
|
233
239
|
|
|
234
240
|
[super unmountChildComponentView:childComponentView index:index];
|
|
@@ -28,6 +28,7 @@ using namespace facebook::react;
|
|
|
28
28
|
CGFloat _bottomInset;
|
|
29
29
|
CGFloat _originalScrollViewHeight;
|
|
30
30
|
CGFloat _originalIndicatorBottomInset;
|
|
31
|
+
BOOL _observingTextChanges;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
@@ -42,6 +43,32 @@ using namespace facebook::react;
|
|
|
42
43
|
return self;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
- (void)dealloc {
|
|
47
|
+
[self stopObservingTextChanges];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#pragma mark - Text Change Observing
|
|
51
|
+
|
|
52
|
+
// The notification is app-wide (object:nil), so only observe while the keyboard is up
|
|
53
|
+
- (void)startObservingTextChanges {
|
|
54
|
+
if (_observingTextChanges) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
_observingTextChanges = YES;
|
|
58
|
+
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
59
|
+
selector:@selector(focusedInputTextDidChange:)
|
|
60
|
+
name:UITextViewTextDidChangeNotification
|
|
61
|
+
object:nil];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
- (void)stopObservingTextChanges {
|
|
65
|
+
if (!_observingTextChanges) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
_observingTextChanges = NO;
|
|
69
|
+
[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidChangeNotification object:nil];
|
|
70
|
+
}
|
|
71
|
+
|
|
45
72
|
#pragma mark - Layout
|
|
46
73
|
|
|
47
74
|
- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
|
|
@@ -250,6 +277,8 @@ using namespace facebook::react;
|
|
|
250
277
|
return;
|
|
251
278
|
}
|
|
252
279
|
|
|
280
|
+
[self startObservingTextChanges];
|
|
281
|
+
|
|
253
282
|
TrueSheetViewController *sheetController = _keyboardObserver.viewController;
|
|
254
283
|
UIView *firstResponder = sheetController ? [sheetController.view findFirstResponder] : nil;
|
|
255
284
|
|
|
@@ -265,15 +294,63 @@ using namespace facebook::react;
|
|
|
265
294
|
// Defer scroll until the next run loop so content insets are applied first
|
|
266
295
|
if (firstResponder) {
|
|
267
296
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
268
|
-
|
|
269
|
-
toView:self->_pinnedScrollView.scrollView];
|
|
270
|
-
responderFrame.size.height += self.keyboardScrollOffset;
|
|
271
|
-
[self->_pinnedScrollView.scrollView scrollRectToVisible:responderFrame animated:YES];
|
|
297
|
+
[self scrollToFocusedCaretAnimated:YES];
|
|
272
298
|
});
|
|
273
299
|
}
|
|
274
300
|
}
|
|
275
301
|
|
|
302
|
+
- (void)focusedInputTextDidChange:(NSNotification *)notification {
|
|
303
|
+
if (!_pinnedScrollView || !_keyboardObserver || _keyboardObserver.currentHeight <= 0) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
TrueSheetViewController *sheetController = _keyboardObserver.viewController;
|
|
308
|
+
UIView *firstResponder = sheetController ? [sheetController.view findFirstResponder] : nil;
|
|
309
|
+
if (!firstResponder || notification.object != firstResponder) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
314
|
+
[self scrollToFocusedCaretAnimated:YES];
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
- (void)scrollToFocusedCaretAnimated:(BOOL)animated {
|
|
319
|
+
if (!_pinnedScrollView) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
TrueSheetViewController *sheetController = _keyboardObserver.viewController;
|
|
324
|
+
UIView *firstResponder = sheetController ? [sheetController.view findFirstResponder] : nil;
|
|
325
|
+
if (!firstResponder) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
UIScrollView *scrollView = _pinnedScrollView.scrollView;
|
|
330
|
+
CGRect targetRect = [firstResponder convertRect:firstResponder.bounds toView:scrollView];
|
|
331
|
+
|
|
332
|
+
if ([firstResponder conformsToProtocol:@protocol(UITextInput)]) {
|
|
333
|
+
id<UITextInput> textInput = (id<UITextInput>)firstResponder;
|
|
334
|
+
UITextRange *selectedRange = textInput.selectedTextRange;
|
|
335
|
+
if (selectedRange) {
|
|
336
|
+
CGRect caretRect = [textInput caretRectForPosition:selectedRange.end];
|
|
337
|
+
// caretRectForPosition: can return non-finite coordinates during layout/selection transitions
|
|
338
|
+
BOOL caretRectValid = !CGRectIsNull(caretRect) && !CGRectIsInfinite(caretRect) && isfinite(caretRect.origin.x) &&
|
|
339
|
+
isfinite(caretRect.origin.y) && isfinite(caretRect.size.width) &&
|
|
340
|
+
isfinite(caretRect.size.height);
|
|
341
|
+
if (caretRectValid) {
|
|
342
|
+
targetRect = [firstResponder convertRect:caretRect toView:scrollView];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
targetRect.size.height += self.keyboardScrollOffset;
|
|
348
|
+
[scrollView scrollRectToVisible:targetRect animated:animated];
|
|
349
|
+
}
|
|
350
|
+
|
|
276
351
|
- (void)keyboardWillHide:(NSTimeInterval)duration curve:(UIViewAnimationOptions)curve {
|
|
352
|
+
[self stopObservingTextChanges];
|
|
353
|
+
|
|
277
354
|
if (!_pinnedScrollView) {
|
|
278
355
|
return;
|
|
279
356
|
}
|
|
@@ -292,6 +369,7 @@ using namespace facebook::react;
|
|
|
292
369
|
|
|
293
370
|
- (void)prepareForRecycle {
|
|
294
371
|
[super prepareForRecycle];
|
|
372
|
+
[self stopObservingTextChanges];
|
|
295
373
|
[self clearScrollable];
|
|
296
374
|
}
|
|
297
375
|
|
package/ios/TrueSheetView.mm
CHANGED
|
@@ -393,6 +393,11 @@ using namespace facebook::react;
|
|
|
393
393
|
_controller.headerHeight = @(headerHeight);
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
+
CGFloat footerHeight = [_containerView footerHeight];
|
|
397
|
+
if (footerHeight > 0) {
|
|
398
|
+
_controller.footerHeight = @(footerHeight);
|
|
399
|
+
}
|
|
400
|
+
|
|
396
401
|
if (_eventEmitter) {
|
|
397
402
|
[TrueSheetLifecycleEvents emitMount:_eventEmitter];
|
|
398
403
|
} else {
|
|
@@ -580,6 +585,8 @@ using namespace facebook::react;
|
|
|
580
585
|
}
|
|
581
586
|
|
|
582
587
|
- (void)containerViewFooterDidChangeSize:(CGSize)newSize {
|
|
588
|
+
_controller.footerHeight = @(newSize.height);
|
|
589
|
+
[self setupSheetDetentsForSizeChange];
|
|
583
590
|
[_controller setupAccessibilityContainer];
|
|
584
591
|
}
|
|
585
592
|
|
|
@@ -57,6 +57,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
57
57
|
@property (nonatomic, strong, nullable) NSNumber *maxContentWidth;
|
|
58
58
|
@property (nonatomic, strong, nullable) NSNumber *contentHeight;
|
|
59
59
|
@property (nonatomic, strong, nullable) NSNumber *headerHeight;
|
|
60
|
+
@property (nonatomic, strong, nullable) NSNumber *footerHeight;
|
|
60
61
|
@property (nonatomic, strong, nullable) UIColor *backgroundColor;
|
|
61
62
|
@property (nonatomic, strong, nullable) NSNumber *cornerRadius;
|
|
62
63
|
@property (nonatomic, assign) BOOL grabber;
|
|
@@ -91,6 +91,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
91
91
|
_detents = @[ @0.5, @1 ];
|
|
92
92
|
_contentHeight = @(0);
|
|
93
93
|
_headerHeight = @(0);
|
|
94
|
+
_footerHeight = @(0);
|
|
94
95
|
_grabber = YES;
|
|
95
96
|
_draggable = YES;
|
|
96
97
|
_scrollingExpandsSheet = YES;
|
|
@@ -898,6 +899,14 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
898
899
|
}
|
|
899
900
|
}
|
|
900
901
|
|
|
902
|
+
if (value == -2) {
|
|
903
|
+
if (@available(iOS 16.0, *)) {
|
|
904
|
+
return [self customDetentWithIdentifier:@"custom-peek" height:[_detentCalculator peekHeight] atIndex:index];
|
|
905
|
+
} else {
|
|
906
|
+
return [UISheetPresentationControllerDetent mediumDetent];
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
901
910
|
if (value <= 0 || value > 1) {
|
|
902
911
|
RCTLogError(@"TrueSheet: detent fraction (%f) must be between 0 and 1", value);
|
|
903
912
|
return [UISheetPresentationControllerDetent mediumDetent];
|
|
@@ -21,6 +21,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
21
21
|
@property (nonatomic, strong, readonly) NSArray<NSNumber *> *detents;
|
|
22
22
|
@property (nonatomic, strong, readonly, nullable) NSNumber *contentHeight;
|
|
23
23
|
@property (nonatomic, strong, readonly, nullable) NSNumber *headerHeight;
|
|
24
|
+
@property (nonatomic, strong, readonly, nullable) NSNumber *footerHeight;
|
|
24
25
|
|
|
25
26
|
@end
|
|
26
27
|
|
|
@@ -36,9 +37,17 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
36
37
|
/**
|
|
37
38
|
Returns the detent value (0-1 fraction) for a given index.
|
|
38
39
|
For auto (-1) detents, calculates based on content + header height.
|
|
40
|
+
For peek (-2) detents, calculates based on header + footer height.
|
|
39
41
|
*/
|
|
40
42
|
- (CGFloat)detentValueForIndex:(NSInteger)index;
|
|
41
43
|
|
|
44
|
+
/**
|
|
45
|
+
Returns the height for peek (-2) detents: header + footer height.
|
|
46
|
+
Falls back to 150 when neither is present — matches the iOS 26
|
|
47
|
+
floating small-detent threshold.
|
|
48
|
+
*/
|
|
49
|
+
- (CGFloat)peekHeight;
|
|
50
|
+
|
|
42
51
|
/**
|
|
43
52
|
Learns the offset between resolver height and actual presented height for a detent.
|
|
44
53
|
Called when the sheet settles at a detent.
|
|
@@ -22,11 +22,19 @@
|
|
|
22
22
|
CGFloat autoHeight = [self.delegate.contentHeight floatValue] + [self.delegate.headerHeight floatValue];
|
|
23
23
|
return autoHeight / self.delegate.screenHeight;
|
|
24
24
|
}
|
|
25
|
+
if (value == -2) {
|
|
26
|
+
return [self peekHeight] / self.delegate.screenHeight;
|
|
27
|
+
}
|
|
25
28
|
return value;
|
|
26
29
|
}
|
|
27
30
|
return 0;
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
- (CGFloat)peekHeight {
|
|
34
|
+
CGFloat height = [self.delegate.headerHeight floatValue] + [self.delegate.footerHeight floatValue];
|
|
35
|
+
return height > 0 ? height : 150;
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
- (void)learnOffsetForDetentIndex:(NSInteger)index {
|
|
31
39
|
if (index < 0 || !_resolvedDetentHeights || index >= (NSInteger)_resolvedDetentHeights.count) {
|
|
32
40
|
return;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Created by Jovanni Lo (@lodev09)
|
|
3
|
+
// Copyright (c) 2024-present. All rights reserved.
|
|
4
|
+
//
|
|
5
|
+
// This source code is licensed under the MIT license found in the
|
|
6
|
+
// LICENSE file in the root directory of this source tree.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
// tvOS stubs. Codegen registers TrueSheet components and looks them up via
|
|
10
|
+
// NSClassFromString on all Apple platforms, so the classes must exist on tvOS
|
|
11
|
+
// even though sheets are not supported there.
|
|
12
|
+
|
|
13
|
+
#import <TargetConditionals.h>
|
|
14
|
+
|
|
15
|
+
#if TARGET_OS_TV && defined(RCT_NEW_ARCH_ENABLED)
|
|
16
|
+
|
|
17
|
+
#import <React/RCTBridgeModule.h>
|
|
18
|
+
#import <React/RCTViewComponentView.h>
|
|
19
|
+
|
|
20
|
+
#import <react/renderer/components/TrueSheetSpec/ComponentDescriptors.h>
|
|
21
|
+
#import <react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h>
|
|
22
|
+
|
|
23
|
+
#import <TrueSheetSpec/TrueSheetSpec.h>
|
|
24
|
+
|
|
25
|
+
using namespace facebook::react;
|
|
26
|
+
|
|
27
|
+
#pragma mark - Component Views
|
|
28
|
+
|
|
29
|
+
@interface TrueSheetView : RCTViewComponentView
|
|
30
|
+
@end
|
|
31
|
+
|
|
32
|
+
@implementation TrueSheetView
|
|
33
|
+
|
|
34
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
35
|
+
return concreteComponentDescriptorProvider<TrueSheetViewComponentDescriptor>();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@end
|
|
39
|
+
|
|
40
|
+
@interface TrueSheetContainerView : RCTViewComponentView
|
|
41
|
+
@end
|
|
42
|
+
|
|
43
|
+
@implementation TrueSheetContainerView
|
|
44
|
+
|
|
45
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
46
|
+
return concreteComponentDescriptorProvider<TrueSheetContainerViewComponentDescriptor>();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@end
|
|
50
|
+
|
|
51
|
+
@interface TrueSheetContentView : RCTViewComponentView
|
|
52
|
+
@end
|
|
53
|
+
|
|
54
|
+
@implementation TrueSheetContentView
|
|
55
|
+
|
|
56
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
57
|
+
return concreteComponentDescriptorProvider<TrueSheetContentViewComponentDescriptor>();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@end
|
|
61
|
+
|
|
62
|
+
@interface TrueSheetHeaderView : RCTViewComponentView
|
|
63
|
+
@end
|
|
64
|
+
|
|
65
|
+
@implementation TrueSheetHeaderView
|
|
66
|
+
|
|
67
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
68
|
+
return concreteComponentDescriptorProvider<TrueSheetHeaderViewComponentDescriptor>();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@end
|
|
72
|
+
|
|
73
|
+
@interface TrueSheetFooterView : RCTViewComponentView
|
|
74
|
+
@end
|
|
75
|
+
|
|
76
|
+
@implementation TrueSheetFooterView
|
|
77
|
+
|
|
78
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
79
|
+
return concreteComponentDescriptorProvider<TrueSheetFooterViewComponentDescriptor>();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@end
|
|
83
|
+
|
|
84
|
+
#pragma mark - Module
|
|
85
|
+
|
|
86
|
+
@interface TrueSheetModule : NSObject <RCTBridgeModule, NativeTrueSheetModuleSpec>
|
|
87
|
+
@end
|
|
88
|
+
|
|
89
|
+
@implementation TrueSheetModule
|
|
90
|
+
|
|
91
|
+
RCT_EXPORT_MODULE(TrueSheetModule)
|
|
92
|
+
|
|
93
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
94
|
+
return NO;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
98
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params {
|
|
99
|
+
return std::make_shared<facebook::react::NativeTrueSheetModuleSpecJSI>(params);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
- (void)presentByRef:(double)viewTag
|
|
103
|
+
index:(double)index
|
|
104
|
+
animated:(BOOL)animated
|
|
105
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
106
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
107
|
+
reject(@"PRESENT_FAILED", @"TrueSheet is not supported on tvOS", nil);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
- (void)dismissByRef:(double)viewTag
|
|
111
|
+
animated:(BOOL)animated
|
|
112
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
113
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
114
|
+
resolve(nil);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
- (void)dismissStackByRef:(double)viewTag
|
|
118
|
+
animated:(BOOL)animated
|
|
119
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
120
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
121
|
+
resolve(nil);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
- (void)resizeByRef:(double)viewTag
|
|
125
|
+
index:(double)index
|
|
126
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
127
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
128
|
+
resolve(nil);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
- (void)handleBackPress:(double)viewTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
132
|
+
resolve(nil);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
- (void)dismissAll:(BOOL)animated resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
136
|
+
resolve(nil);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@end
|
|
140
|
+
|
|
141
|
+
#endif // TARGET_OS_TV && RCT_NEW_ARCH_ENABLED
|
package/lib/module/TrueSheet.js
CHANGED
|
@@ -77,7 +77,7 @@ export class TrueSheet extends PureComponent {
|
|
|
77
77
|
// Warn for invalid detent fractions
|
|
78
78
|
if (detents) {
|
|
79
79
|
detents.forEach((detent, index) => {
|
|
80
|
-
if (detent
|
|
80
|
+
if (typeof detent === 'number' && detent !== -1 && detent !== -2) {
|
|
81
81
|
if (detent <= 0 || detent > 1) {
|
|
82
82
|
console.warn(`TrueSheet: detent at index ${index} (${detent}) should be between 0 and 1. It will be clamped.`);
|
|
83
83
|
}
|
|
@@ -369,6 +369,7 @@ export class TrueSheet extends PureComponent {
|
|
|
369
369
|
// Trim to max 3 detents and clamp fractions
|
|
370
370
|
const resolvedDetents = detents.slice(0, 3).map(detent => {
|
|
371
371
|
if (detent === 'auto' || detent === -1) return -1;
|
|
372
|
+
if (detent === 'peek' || detent === -2) return -2;
|
|
372
373
|
|
|
373
374
|
// Default to 0.1 if zero or below
|
|
374
375
|
if (detent <= 0) return 0.1;
|