@lodev09/react-native-true-sheet 3.11.3 → 3.11.5

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.
Files changed (71) hide show
  1. package/RNTrueSheet.podspec +6 -2
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +69 -1
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +62 -4
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +2 -1
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetPeekView.kt +72 -0
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetPeekViewManager.kt +33 -0
  7. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +7 -1
  8. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +10 -0
  9. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +24 -1
  10. package/ios/TrueSheetContainerView.h +18 -0
  11. package/ios/TrueSheetContainerView.mm +78 -1
  12. package/ios/TrueSheetContentView.mm +82 -4
  13. package/ios/TrueSheetPeekView.h +32 -0
  14. package/ios/TrueSheetPeekView.mm +99 -0
  15. package/ios/TrueSheetView.mm +19 -0
  16. package/ios/TrueSheetViewController.h +2 -0
  17. package/ios/TrueSheetViewController.mm +10 -0
  18. package/ios/core/TrueSheetDetentCalculator.h +10 -0
  19. package/ios/core/TrueSheetDetentCalculator.mm +9 -0
  20. package/ios/tvos/TrueSheetStubs.mm +152 -0
  21. package/lib/module/TrueSheet.js +2 -1
  22. package/lib/module/TrueSheet.js.map +1 -1
  23. package/lib/module/TrueSheet.web.js +104 -69
  24. package/lib/module/TrueSheet.web.js.map +1 -1
  25. package/lib/module/TrueSheetPeek.js +15 -0
  26. package/lib/module/TrueSheetPeek.js.map +1 -0
  27. package/lib/module/TrueSheetPeek.web.js +46 -0
  28. package/lib/module/TrueSheetPeek.web.js.map +1 -0
  29. package/lib/module/fabric/TrueSheetPeekViewNativeComponent.ts +8 -0
  30. package/lib/module/index.js +1 -0
  31. package/lib/module/index.js.map +1 -1
  32. package/lib/module/mocks/index.js +10 -0
  33. package/lib/module/mocks/index.js.map +1 -1
  34. package/lib/module/web/vaul/constants.js +1 -0
  35. package/lib/module/web/vaul/constants.js.map +1 -1
  36. package/lib/module/web/vaul/index.js +2 -0
  37. package/lib/module/web/vaul/index.js.map +1 -1
  38. package/lib/module/web/vaul/use-snap-points.js +6 -4
  39. package/lib/module/web/vaul/use-snap-points.js.map +1 -1
  40. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  41. package/lib/typescript/src/TrueSheet.types.d.ts +10 -0
  42. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  43. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  44. package/lib/typescript/src/TrueSheetPeek.d.ts +9 -0
  45. package/lib/typescript/src/TrueSheetPeek.d.ts.map +1 -0
  46. package/lib/typescript/src/TrueSheetPeek.web.d.ts +19 -0
  47. package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -0
  48. package/lib/typescript/src/fabric/TrueSheetPeekViewNativeComponent.d.ts +6 -0
  49. package/lib/typescript/src/fabric/TrueSheetPeekViewNativeComponent.d.ts.map +1 -0
  50. package/lib/typescript/src/index.d.ts +1 -0
  51. package/lib/typescript/src/index.d.ts.map +1 -1
  52. package/lib/typescript/src/mocks/index.d.ts +6 -2
  53. package/lib/typescript/src/mocks/index.d.ts.map +1 -1
  54. package/lib/typescript/src/web/vaul/constants.d.ts +1 -0
  55. package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -1
  56. package/lib/typescript/src/web/vaul/index.d.ts +6 -1
  57. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
  58. package/lib/typescript/src/web/vaul/use-snap-points.d.ts +2 -1
  59. package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
  60. package/package.json +4 -1
  61. package/src/TrueSheet.tsx +2 -1
  62. package/src/TrueSheet.types.ts +11 -0
  63. package/src/TrueSheet.web.tsx +110 -71
  64. package/src/TrueSheetPeek.tsx +11 -0
  65. package/src/TrueSheetPeek.web.tsx +48 -0
  66. package/src/fabric/TrueSheetPeekViewNativeComponent.ts +8 -0
  67. package/src/index.ts +1 -0
  68. package/src/mocks/index.ts +8 -1
  69. package/src/web/vaul/constants.ts +2 -0
  70. package/src/web/vaul/index.tsx +7 -0
  71. package/src/web/vaul/use-snap-points.ts +17 -4
@@ -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
- s.source_files = "ios/**/*.{h,m,mm,swift,cpp}", "common/cpp/**/*.{cpp,h}"
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 = {
@@ -2,8 +2,10 @@ package com.lodev09.truesheet
2
2
 
3
3
  import android.annotation.SuppressLint
4
4
  import android.view.View
5
+ import android.view.ViewGroup
5
6
  import com.facebook.react.uimanager.ThemedReactContext
6
7
  import com.facebook.react.uimanager.events.EventDispatcher
8
+ import com.facebook.react.util.RNLog
7
9
  import com.facebook.react.views.view.ReactViewGroup
8
10
 
9
11
  interface TrueSheetContainerViewDelegate {
@@ -13,6 +15,7 @@ interface TrueSheetContainerViewDelegate {
13
15
  fun containerViewScrollViewDidChange()
14
16
  fun containerViewHeaderDidChangeSize(width: Int, height: Int)
15
17
  fun containerViewFooterDidChangeSize(width: Int, height: Int)
18
+ fun containerViewPeekDidChangeSize(width: Int, height: Int)
16
19
  }
17
20
 
18
21
  /**
@@ -24,18 +27,40 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
24
27
  ReactViewGroup(reactContext),
25
28
  TrueSheetContentViewDelegate,
26
29
  TrueSheetHeaderViewDelegate,
27
- TrueSheetFooterViewDelegate {
30
+ TrueSheetFooterViewDelegate,
31
+ TrueSheetPeekViewDelegate {
28
32
 
29
33
  var delegate: TrueSheetContainerViewDelegate? = null
30
34
 
31
35
  var contentView: TrueSheetContentView? = null
32
36
  var headerView: TrueSheetHeaderView? = null
33
37
  var footerView: TrueSheetFooterView? = null
38
+ var peekView: TrueSheetPeekView? = null
34
39
 
35
40
  var contentHeight: Int = 0
36
41
  var headerHeight: Int = 0
37
42
  var footerHeight: Int = 0
38
43
 
44
+ /**
45
+ * Distance from the top of the content view to the bottom of the peek view.
46
+ * Includes the peek view's offset within the content (padding, views above it)
47
+ * so the peek detent reveals everything down to the peek content's bottom edge.
48
+ */
49
+ val peekContentHeight: Int
50
+ get() {
51
+ val peek = peekView ?: return 0
52
+ val content = contentView ?: return peek.height
53
+
54
+ var bottom = peek.height
55
+ var view: View = peek
56
+ while (view !== content) {
57
+ bottom += view.top
58
+ view = view.parent as? View ?: return peek.height
59
+ }
60
+
61
+ return bottom
62
+ }
63
+
39
64
  var insetAdjustment: TrueSheetInsetAdjustment = TrueSheetInsetAdjustment.AUTOMATIC
40
65
  var scrollViewBottomInset: Int = 0
41
66
  var scrollableEnabled: Boolean = false
@@ -75,6 +100,10 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
75
100
  child.delegate = this
76
101
  child.scrollableOptions = scrollableOptions
77
102
  contentView = child
103
+
104
+ // Children mount bottom-up, so the content subtree is complete here.
105
+ // Late-mounted peek views attach themselves instead (see TrueSheetPeekView).
106
+ findPeekView(child)?.let { attachPeekView(it) }
78
107
  }
79
108
 
80
109
  is TrueSheetHeaderView -> {
@@ -113,6 +142,41 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
113
142
  super.removeViewAt(index)
114
143
  }
115
144
 
145
+ // ==================== Peek View ====================
146
+
147
+ private fun findPeekView(view: View): TrueSheetPeekView? {
148
+ if (view is TrueSheetPeekView) return view
149
+
150
+ if (view is ViewGroup) {
151
+ for (i in 0 until view.childCount) {
152
+ findPeekView(view.getChildAt(i))?.let { return it }
153
+ }
154
+ }
155
+
156
+ return null
157
+ }
158
+
159
+ fun attachPeekView(view: TrueSheetPeekView) {
160
+ if (peekView === view) return
161
+
162
+ if (peekView != null) {
163
+ RNLog.w(context as ThemedReactContext, "TrueSheet: Sheet can only have one peek component.")
164
+ return
165
+ }
166
+
167
+ peekView = view
168
+ view.delegate = this
169
+ peekViewDidChangeSize(view.width, view.height)
170
+ }
171
+
172
+ fun detachPeekView(view: TrueSheetPeekView) {
173
+ if (peekView !== view) return
174
+
175
+ view.delegate = null
176
+ peekView = null
177
+ peekViewDidChangeSize(0, 0)
178
+ }
179
+
116
180
  // ==================== Delegate Implementations ====================
117
181
 
118
182
  override fun contentViewDidChangeSize(width: Int, height: Int) {
@@ -138,6 +202,10 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
138
202
  delegate?.containerViewFooterDidChangeSize(width, height)
139
203
  }
140
204
 
205
+ override fun peekViewDidChangeSize(width: Int, height: Int) {
206
+ delegate?.containerViewPeekDidChangeSize(width, height)
207
+ }
208
+
141
209
  companion object {
142
210
  const val TAG_NAME = "TrueSheet"
143
211
  }
@@ -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 relativeTop = focusedLocation[1] - scrollViewLocation[1] + scrollView.scrollY
238
- val relativeBottom = relativeTop + focusedView.height + keyboardScrollOffset.toInt()
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
- if (relativeBottom > visibleBottom) {
244
- scrollView.smoothScrollTo(0, relativeBottom - visibleHeight)
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
 
@@ -39,6 +39,7 @@ class TrueSheetPackage : BaseReactPackage() {
39
39
  TrueSheetContainerViewManager(),
40
40
  TrueSheetContentViewManager(),
41
41
  TrueSheetHeaderViewManager(),
42
- TrueSheetFooterViewManager()
42
+ TrueSheetFooterViewManager(),
43
+ TrueSheetPeekViewManager()
43
44
  )
44
45
  }
@@ -0,0 +1,72 @@
1
+ package com.lodev09.truesheet
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.view.ViewParent
5
+ import com.facebook.react.uimanager.ThemedReactContext
6
+ import com.facebook.react.views.view.ReactViewGroup
7
+
8
+ /**
9
+ * Delegate interface for peek view size changes
10
+ */
11
+ interface TrueSheetPeekViewDelegate {
12
+ fun peekViewDidChangeSize(width: Int, height: Int)
13
+ }
14
+
15
+ /**
16
+ * Peek view that marks part of the content as the sheet's peek content.
17
+ * The peek detent covers the content through this view's bottom edge.
18
+ */
19
+ @SuppressLint("ViewConstructor")
20
+ class TrueSheetPeekView(context: ThemedReactContext) : ReactViewGroup(context) {
21
+ var delegate: TrueSheetPeekViewDelegate? = null
22
+
23
+ override fun onLayout(
24
+ changed: Boolean,
25
+ left: Int,
26
+ top: Int,
27
+ right: Int,
28
+ bottom: Int
29
+ ) {
30
+ super.onLayout(changed, left, top, right, bottom)
31
+
32
+ attachToContainerView()
33
+
34
+ // Position changes matter too — the peek's offset within the content
35
+ // affects the peek detent.
36
+ if (changed) {
37
+ delegate?.peekViewDidChangeSize(width, height)
38
+ }
39
+ }
40
+
41
+ override fun onAttachedToWindow() {
42
+ super.onAttachedToWindow()
43
+ attachToContainerView()
44
+ }
45
+
46
+ /**
47
+ * Peek can be nested anywhere within the content, so it attaches itself to the
48
+ * nearest container instead of being mounted by it. The container also searches
49
+ * for it when the content is added to cover the initial (bottom-up) mount order.
50
+ */
51
+ private fun attachToContainerView() {
52
+ if (delegate != null) return
53
+
54
+ var parent: ViewParent? = parent
55
+ while (parent != null && parent !is TrueSheetContainerView) {
56
+ parent = parent.parent
57
+ }
58
+
59
+ (parent as? TrueSheetContainerView)?.attachPeekView(this)
60
+ }
61
+
62
+ /**
63
+ * Called by the ViewManager when the view is dropped (Fabric unmount)
64
+ */
65
+ fun detachFromContainerView() {
66
+ (delegate as? TrueSheetContainerView)?.detachPeekView(this)
67
+ }
68
+
69
+ companion object {
70
+ const val TAG_NAME = "TrueSheet"
71
+ }
72
+ }
@@ -0,0 +1,33 @@
1
+ package com.lodev09.truesheet
2
+
3
+ import com.facebook.react.module.annotations.ReactModule
4
+ import com.facebook.react.uimanager.PointerEvents
5
+ import com.facebook.react.uimanager.ThemedReactContext
6
+ import com.facebook.react.uimanager.ViewGroupManager
7
+ import com.facebook.react.uimanager.annotations.ReactProp
8
+
9
+ /**
10
+ * ViewManager for TrueSheetPeekView
11
+ * Marks part of the content as the sheet's peek content
12
+ */
13
+ @ReactModule(name = TrueSheetPeekViewManager.REACT_CLASS)
14
+ class TrueSheetPeekViewManager : ViewGroupManager<TrueSheetPeekView>() {
15
+
16
+ override fun getName(): String = REACT_CLASS
17
+
18
+ override fun createViewInstance(reactContext: ThemedReactContext): TrueSheetPeekView = TrueSheetPeekView(reactContext)
19
+
20
+ override fun onDropViewInstance(view: TrueSheetPeekView) {
21
+ super.onDropViewInstance(view)
22
+ view.detachFromContainerView()
23
+ }
24
+
25
+ @ReactProp(name = "pointerEvents")
26
+ fun setPointerEvents(view: TrueSheetPeekView, pointerEventsStr: String?) {
27
+ view.pointerEvents = PointerEvents.parsePointerEvents(pointerEventsStr)
28
+ }
29
+
30
+ companion object {
31
+ const val REACT_CLASS = "TrueSheetPeekView"
32
+ }
33
+ }
@@ -615,10 +615,16 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
615
615
  }
616
616
 
617
617
  override fun containerViewFooterDidChangeSize(width: Int, height: Int) {
618
- // Footer changes don't affect detents, only reposition it
618
+ // Footer height affects peek detents
619
+ updateSheetIfNeeded()
619
620
  viewController.positionFooter()
620
621
  }
621
622
 
623
+ override fun containerViewPeekDidChangeSize(width: Int, height: Int) {
624
+ // Peek content height affects peek detents
625
+ updateSheetIfNeeded()
626
+ }
627
+
622
628
  // ==================== RNScreensEventObserverDelegate ====================
623
629
 
624
630
  override fun presenterScreenWillDisappear() {
@@ -275,6 +275,8 @@ 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
279
+ private var cachedPeekContentHeight: Int = 0
278
280
 
279
281
  override val contentHeight: Int
280
282
  get() = containerView?.contentHeight ?: cachedContentHeight
@@ -282,6 +284,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
282
284
  override val headerHeight: Int
283
285
  get() = containerView?.headerHeight ?: cachedHeaderHeight
284
286
 
287
+ override val footerHeight: Int
288
+ get() = containerView?.footerHeight ?: cachedFooterHeight
289
+
290
+ override val peekContentHeight: Int
291
+ get() = containerView?.peekContentHeight ?: cachedPeekContentHeight
292
+
285
293
  // Insets
286
294
  // Target keyboard height used for detent calculations
287
295
  override val keyboardInset: Int
@@ -836,6 +844,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
836
844
  containerView?.let {
837
845
  cachedContentHeight = it.contentHeight
838
846
  cachedHeaderHeight = it.headerHeight
847
+ cachedFooterHeight = it.footerHeight
848
+ cachedPeekContentHeight = it.peekContentHeight
839
849
  }
840
850
 
841
851
  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,8 @@ interface TrueSheetDetentCalculatorDelegate {
14
15
  val detents: MutableList<Double>
15
16
  val contentHeight: Int
16
17
  val headerHeight: Int
18
+ val footerHeight: Int
19
+ val peekContentHeight: Int
17
20
  val contentBottomInset: Int
18
21
  val maxContentHeight: Int?
19
22
  val keyboardInset: Int
@@ -31,17 +34,31 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
31
34
  private val detents: List<Double> get() = delegate?.detents ?: emptyList()
32
35
  private val contentHeight: Int get() = delegate?.contentHeight ?: 0
33
36
  private val headerHeight: Int get() = delegate?.headerHeight ?: 0
37
+ private val footerHeight: Int get() = delegate?.footerHeight ?: 0
38
+ private val peekContentHeight: Int get() = delegate?.peekContentHeight ?: 0
34
39
  private val contentBottomInset: Int get() = delegate?.contentBottomInset ?: 0
35
40
  private val maxContentHeight: Int? get() = delegate?.maxContentHeight
36
41
  private val keyboardInset: Int get() = delegate?.keyboardInset ?: 0
37
42
 
43
+ /**
44
+ * Height for peek (-2.0) detents: header + footer + peek content height.
45
+ * Falls back to 150dp when none is present.
46
+ */
47
+ private val peekDetentHeight: Int
48
+ get() {
49
+ val height = headerHeight + footerHeight + peekContentHeight
50
+ return if (height > 0) height else DEFAULT_PEEK_HEIGHT.dpToPx().toInt()
51
+ }
52
+
38
53
  /**
39
54
  * 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
55
+ * @param detent The detent value: -1.0 for content-fit, -2.0 for peek, or 0.0-1.0 for screen fraction
41
56
  */
42
57
  fun getDetentHeight(detent: Double, includeKeyboard: Boolean = true): Int {
43
58
  val baseHeight = if (detent == -1.0) {
44
59
  contentHeight + headerHeight + contentBottomInset
60
+ } else if (detent == -2.0) {
61
+ peekDetentHeight + contentBottomInset
45
62
  } else {
46
63
  if (detent <= 0.0 || detent > 1.0) {
47
64
  throw IllegalArgumentException("TrueSheet: detent fraction ($detent) must be between 0 and 1")
@@ -83,6 +100,8 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
83
100
  val value = detents[index]
84
101
  return if (value == -1.0) {
85
102
  (contentHeight + headerHeight).toFloat() / screenHeight.toFloat()
103
+ } else if (value == -2.0) {
104
+ peekDetentHeight.toFloat() / screenHeight.toFloat()
86
105
  } else {
87
106
  value.toFloat()
88
107
  }
@@ -211,4 +230,8 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
211
230
  val toDetent = getDetentValueForIndex(toIndex)
212
231
  return fromDetent + progress * (toDetent - fromDetent)
213
232
  }
233
+
234
+ companion object {
235
+ private const val DEFAULT_PEEK_HEIGHT = 150f
236
+ }
214
237
  }
@@ -23,6 +23,8 @@ NS_ASSUME_NONNULL_BEGIN
23
23
 
24
24
  @end
25
25
 
26
+ @class TrueSheetPeekView;
27
+
26
28
  @protocol TrueSheetContainerViewDelegate <NSObject>
27
29
 
28
30
  - (void)containerViewContentDidChangeSize:(CGSize)newSize;
@@ -32,6 +34,7 @@ NS_ASSUME_NONNULL_BEGIN
32
34
 
33
35
  - (void)containerViewHeaderDidChangeSize:(CGSize)newSize;
34
36
  - (void)containerViewFooterDidChangeSize:(CGSize)newSize;
37
+ - (void)containerViewPeekDidChangeSize:(CGSize)newSize;
35
38
 
36
39
  @end
37
40
 
@@ -67,6 +70,21 @@ NS_ASSUME_NONNULL_BEGIN
67
70
  */
68
71
  - (CGFloat)headerHeight;
69
72
 
73
+ /**
74
+ * Returns the current footer height
75
+ */
76
+ - (CGFloat)footerHeight;
77
+
78
+ /**
79
+ * Returns the current height of the peek view within the content
80
+ */
81
+ - (CGFloat)peekContentHeight;
82
+
83
+ /**
84
+ * Registers a peek view found within the content subtree
85
+ */
86
+ - (void)attachPeekView:(TrueSheetPeekView *)peekView;
87
+
70
88
  /**
71
89
  * Updates footer layout constraints if needed
72
90
  */
@@ -13,6 +13,7 @@
13
13
  #import "TrueSheetContentView.h"
14
14
  #import "TrueSheetFooterView.h"
15
15
  #import "TrueSheetHeaderView.h"
16
+ #import "TrueSheetPeekView.h"
16
17
  #import "TrueSheetViewController.h"
17
18
  #import "core/TrueSheetKeyboardObserver.h"
18
19
  #import "utils/UIView+ScrollEdgeInteraction.h"
@@ -45,13 +46,15 @@ using namespace facebook::react;
45
46
 
46
47
  @interface TrueSheetContainerView () <TrueSheetContentViewDelegate,
47
48
  TrueSheetHeaderViewDelegate,
48
- TrueSheetFooterViewDelegate>
49
+ TrueSheetFooterViewDelegate,
50
+ TrueSheetPeekViewDelegate>
49
51
  @end
50
52
 
51
53
  @implementation TrueSheetContainerView {
52
54
  TrueSheetContentView *_contentView;
53
55
  TrueSheetHeaderView *_headerView;
54
56
  TrueSheetFooterView *_footerView;
57
+ TrueSheetPeekView *__weak _peekView;
55
58
  TrueSheetKeyboardObserver *_keyboardObserver;
56
59
  BOOL _scrollableSet;
57
60
  }
@@ -119,6 +122,25 @@ using namespace facebook::react;
119
122
  return _headerView ? _headerView.frame.size.height : 0;
120
123
  }
121
124
 
125
+ - (CGFloat)footerHeight {
126
+ return _footerView ? _footerView.frame.size.height : 0;
127
+ }
128
+
129
+ // Distance from the top of the content view to the bottom of the peek view.
130
+ // Includes the peek view's offset within the content (padding, views above it)
131
+ // so the peek detent reveals everything down to the peek content's bottom edge.
132
+ - (CGFloat)peekContentHeight {
133
+ if (!_peekView) {
134
+ return 0;
135
+ }
136
+
137
+ if (!_contentView || ![_peekView isDescendantOfView:_contentView]) {
138
+ return _peekView.frame.size.height;
139
+ }
140
+
141
+ return CGRectGetMaxY([_peekView convertRect:_peekView.bounds toView:_contentView]);
142
+ }
143
+
122
144
  - (void)layoutFooter {
123
145
  if (_footerView) {
124
146
  CGFloat height = _footerView.frame.size.height;
@@ -192,6 +214,13 @@ using namespace facebook::react;
192
214
  }
193
215
  _contentView = (TrueSheetContentView *)childComponentView;
194
216
  _contentView.delegate = self;
217
+
218
+ // Children mount bottom-up, so the content subtree is complete here.
219
+ // Late-mounted peek views attach themselves instead (see TrueSheetPeekView).
220
+ TrueSheetPeekView *peekView = [self findPeekViewIn:_contentView];
221
+ if (peekView) {
222
+ [self attachPeekView:peekView];
223
+ }
195
224
  }
196
225
 
197
226
  if ([childComponentView isKindOfClass:[TrueSheetHeaderView class]]) {
@@ -211,6 +240,7 @@ using namespace facebook::react;
211
240
  }
212
241
  _footerView = (TrueSheetFooterView *)childComponentView;
213
242
  _footerView.delegate = self;
243
+ [self footerViewDidChangeSize:_footerView.frame.size];
214
244
  }
215
245
  }
216
246
 
@@ -229,6 +259,7 @@ using namespace facebook::react;
229
259
  if ([childComponentView isKindOfClass:[TrueSheetFooterView class]]) {
230
260
  _footerView.delegate = nil;
231
261
  _footerView = nil;
262
+ [self footerViewDidChangeSize:CGSizeZero];
232
263
  }
233
264
 
234
265
  [super unmountChildComponentView:childComponentView index:index];
@@ -272,6 +303,52 @@ using namespace facebook::react;
272
303
  }
273
304
  }
274
305
 
306
+ #pragma mark - TrueSheetPeekViewDelegate
307
+
308
+ - (nullable TrueSheetPeekView *)findPeekViewIn:(UIView *)view {
309
+ if ([view isKindOfClass:[TrueSheetPeekView class]]) {
310
+ return (TrueSheetPeekView *)view;
311
+ }
312
+
313
+ for (UIView *subview in view.subviews) {
314
+ TrueSheetPeekView *found = [self findPeekViewIn:subview];
315
+ if (found) {
316
+ return found;
317
+ }
318
+ }
319
+
320
+ return nil;
321
+ }
322
+
323
+ - (void)attachPeekView:(TrueSheetPeekView *)peekView {
324
+ if (_peekView == peekView) {
325
+ return;
326
+ }
327
+
328
+ if (_peekView != nil) {
329
+ RCTLogWarn(@"TrueSheet: Sheet can only have one peek component.");
330
+ return;
331
+ }
332
+
333
+ _peekView = peekView;
334
+ peekView.delegate = self;
335
+ [self peekViewDidChangeSize:peekView.frame.size];
336
+ }
337
+
338
+ - (void)peekViewDidChangeSize:(CGSize)newSize {
339
+ [self.delegate containerViewPeekDidChangeSize:newSize];
340
+ }
341
+
342
+ - (void)peekViewWillDetach:(TrueSheetPeekView *)peekView {
343
+ if (_peekView != peekView) {
344
+ return;
345
+ }
346
+
347
+ _peekView.delegate = nil;
348
+ _peekView = nil;
349
+ [self peekViewDidChangeSize:CGSizeZero];
350
+ }
351
+
275
352
  #pragma mark - Keyboard Observer
276
353
 
277
354
  - (void)setupKeyboardObserverWithViewController:(UIViewController *)viewController {