@lodev09/react-native-true-sheet 4.0.0-beta.2 → 4.0.0-beta.21

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 (205) hide show
  1. package/README.md +25 -4
  2. package/android/build.gradle +22 -1
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +50 -16
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +303 -184
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +5 -1
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +21 -110
  7. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +116 -0
  8. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayViewManager.kt +45 -0
  9. package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +2 -1
  10. package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +18 -0
  11. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +124 -50
  12. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +369 -163
  13. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +59 -12
  14. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetBehavior.kt +126 -0
  15. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +22 -9
  16. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +84 -5
  17. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +104 -39
  18. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +3 -0
  19. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetOverlayRootView.kt +137 -0
  20. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +15 -0
  21. package/android/src/main/java/com/lodev09/truesheet/utils/KeyboardUtils.kt +15 -2
  22. package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +0 -12
  23. package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +19 -7
  24. package/android/src/main/jni/TrueSheetSpec.h +1 -0
  25. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +1 -9
  26. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +110 -30
  27. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +30 -7
  28. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +2 -2
  29. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +9 -8
  30. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h +3 -5
  31. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +9 -3
  32. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +3 -6
  33. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +1 -1
  34. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +4 -6
  35. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.cpp +1 -1
  36. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.h +1 -1
  37. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetLayoutUtils.h +43 -0
  38. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewComponentDescriptor.h +21 -0
  39. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.cpp +17 -0
  40. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.h +33 -0
  41. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h +3 -5
  42. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +7 -40
  43. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.h +3 -7
  44. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.cpp +3 -5
  45. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.h +4 -5
  46. package/ios/TrueSheetContainerView.h +27 -7
  47. package/ios/TrueSheetContainerView.mm +31 -11
  48. package/ios/TrueSheetContentView.h +43 -15
  49. package/ios/TrueSheetContentView.mm +211 -189
  50. package/ios/TrueSheetFooterView.h +6 -0
  51. package/ios/TrueSheetFooterView.mm +93 -17
  52. package/ios/TrueSheetModule.h +2 -7
  53. package/ios/TrueSheetModule.mm +7 -16
  54. package/ios/TrueSheetOverlayView.h +32 -0
  55. package/ios/TrueSheetOverlayView.mm +161 -0
  56. package/ios/TrueSheetView.mm +275 -56
  57. package/ios/TrueSheetViewController.h +31 -3
  58. package/ios/TrueSheetViewController.mm +393 -94
  59. package/ios/core/TrueSheetBlurView.h +0 -2
  60. package/ios/core/TrueSheetBlurView.mm +3 -36
  61. package/ios/core/TrueSheetDetentCalculator.h +7 -1
  62. package/ios/core/TrueSheetDetentCalculator.mm +27 -4
  63. package/ios/core/TrueSheetGrabberView.mm +13 -3
  64. package/ios/core/TrueSheetKeyboardObserver.mm +2 -0
  65. package/ios/core/TrueSheetOverlayContainerView.h +31 -0
  66. package/ios/core/TrueSheetOverlayContainerView.mm +35 -0
  67. package/ios/events/TrueSheetLifecycleEvents.h +2 -0
  68. package/ios/events/TrueSheetLifecycleEvents.mm +8 -0
  69. package/ios/tvos/TrueSheetStubs.mm +12 -0
  70. package/ios/utils/UIView+ScrollEdgeInteraction.h +1 -0
  71. package/ios/utils/UIView+ScrollEdgeInteraction.mm +24 -2
  72. package/lib/module/TrueSheet.js +175 -39
  73. package/lib/module/TrueSheet.js.map +1 -1
  74. package/lib/module/TrueSheet.web.js +220 -243
  75. package/lib/module/TrueSheet.web.js.map +1 -1
  76. package/lib/module/TrueSheetBackground.js +15 -0
  77. package/lib/module/TrueSheetBackground.js.map +1 -0
  78. package/lib/module/TrueSheetOverlay.js +30 -0
  79. package/lib/module/TrueSheetOverlay.js.map +1 -0
  80. package/lib/module/TrueSheetOverlay.web.js +35 -0
  81. package/lib/module/TrueSheetOverlay.web.js.map +1 -0
  82. package/lib/module/TrueSheetPeek.web.js +20 -15
  83. package/lib/module/TrueSheetPeek.web.js.map +1 -1
  84. package/lib/module/detentBackgrounds.js +21 -0
  85. package/lib/module/detentBackgrounds.js.map +1 -0
  86. package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  87. package/lib/module/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  88. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +15 -10
  89. package/lib/module/index.js +1 -0
  90. package/lib/module/index.js.map +1 -1
  91. package/lib/module/mocks/index.js +18 -2
  92. package/lib/module/mocks/index.js.map +1 -1
  93. package/lib/module/mocks/navigationExpoRouter.js.map +1 -1
  94. package/lib/module/navigation/base-types.js +2 -0
  95. package/lib/module/navigation/base-types.js.map +1 -0
  96. package/lib/module/navigation/createTrueSheetRouter.js +0 -9
  97. package/lib/module/navigation/createTrueSheetRouter.js.map +1 -1
  98. package/lib/module/navigation/expo-router/base-types.js +2 -0
  99. package/lib/module/navigation/expo-router/base-types.js.map +1 -0
  100. package/lib/module/navigation/expo-router/index.js +10 -7
  101. package/lib/module/navigation/expo-router/index.js.map +1 -1
  102. package/lib/module/navigation/expo-router/types.js +4 -0
  103. package/lib/module/navigation/expo-router/types.js.map +1 -0
  104. package/lib/module/navigation/index.js.map +1 -1
  105. package/lib/module/navigation/screen/useSheetScreenState.js +1 -0
  106. package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -1
  107. package/lib/module/web/constants.js +1 -1
  108. package/lib/module/web/constants.js.map +1 -1
  109. package/lib/module/web/layout.js +168 -0
  110. package/lib/module/web/layout.js.map +1 -0
  111. package/lib/module/web/vaul/constants.js +9 -0
  112. package/lib/module/web/vaul/constants.js.map +1 -1
  113. package/lib/module/web/vaul/context.js.map +1 -1
  114. package/lib/module/web/vaul/helpers.js +1 -0
  115. package/lib/module/web/vaul/helpers.js.map +1 -1
  116. package/lib/module/web/vaul/index.js +47 -7
  117. package/lib/module/web/vaul/index.js.map +1 -1
  118. package/lib/module/web/vaul/use-snap-points.js +21 -10
  119. package/lib/module/web/vaul/use-snap-points.js.map +1 -1
  120. package/lib/typescript/src/TrueSheet.d.ts +6 -0
  121. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  122. package/lib/typescript/src/TrueSheet.types.d.ts +157 -34
  123. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  124. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  125. package/lib/typescript/src/TrueSheetBackground.d.ts +3 -0
  126. package/lib/typescript/src/TrueSheetBackground.d.ts.map +1 -0
  127. package/lib/typescript/src/TrueSheetOverlay.d.ts +8 -0
  128. package/lib/typescript/src/TrueSheetOverlay.d.ts.map +1 -0
  129. package/lib/typescript/src/TrueSheetOverlay.web.d.ts +8 -0
  130. package/lib/typescript/src/TrueSheetOverlay.web.d.ts.map +1 -0
  131. package/lib/typescript/src/TrueSheetPeek.web.d.ts +8 -4
  132. package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
  133. package/lib/typescript/src/detentBackgrounds.d.ts +10 -0
  134. package/lib/typescript/src/detentBackgrounds.d.ts.map +1 -0
  135. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts +6 -0
  136. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts.map +1 -0
  137. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +12 -7
  138. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  139. package/lib/typescript/src/index.d.ts +1 -0
  140. package/lib/typescript/src/index.d.ts.map +1 -1
  141. package/lib/typescript/src/mocks/index.d.ts +4 -0
  142. package/lib/typescript/src/mocks/index.d.ts.map +1 -1
  143. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +3 -3
  144. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -1
  145. package/lib/typescript/src/navigation/base-types.d.ts +9 -0
  146. package/lib/typescript/src/navigation/base-types.d.ts.map +1 -0
  147. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +2 -13
  148. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -1
  149. package/lib/typescript/src/navigation/expo-router/base-types.d.ts +8 -0
  150. package/lib/typescript/src/navigation/expo-router/base-types.d.ts.map +1 -0
  151. package/lib/typescript/src/navigation/expo-router/index.d.ts +8 -6
  152. package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -1
  153. package/lib/typescript/src/navigation/expo-router/types.d.ts +44 -0
  154. package/lib/typescript/src/navigation/expo-router/types.d.ts.map +1 -0
  155. package/lib/typescript/src/navigation/index.d.ts +3 -4
  156. package/lib/typescript/src/navigation/index.d.ts.map +1 -1
  157. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +2 -1
  158. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -1
  159. package/lib/typescript/src/navigation/types.d.ts +41 -5
  160. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  161. package/lib/typescript/src/web/constants.d.ts +1 -1
  162. package/lib/typescript/src/web/constants.d.ts.map +1 -1
  163. package/lib/typescript/src/web/layout.d.ts +19 -0
  164. package/lib/typescript/src/web/layout.d.ts.map +1 -0
  165. package/lib/typescript/src/web/vaul/constants.d.ts +3 -0
  166. package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -1
  167. package/lib/typescript/src/web/vaul/context.d.ts +1 -0
  168. package/lib/typescript/src/web/vaul/context.d.ts.map +1 -1
  169. package/lib/typescript/src/web/vaul/helpers.d.ts +1 -0
  170. package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
  171. package/lib/typescript/src/web/vaul/index.d.ts +13 -1
  172. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
  173. package/lib/typescript/src/web/vaul/use-snap-points.d.ts +2 -1
  174. package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
  175. package/package.json +17 -8
  176. package/react-native.config.js +1 -0
  177. package/src/TrueSheet.tsx +208 -48
  178. package/src/TrueSheet.types.ts +166 -35
  179. package/src/TrueSheet.web.tsx +272 -290
  180. package/src/TrueSheetBackground.tsx +17 -0
  181. package/src/TrueSheetOverlay.tsx +24 -0
  182. package/src/TrueSheetOverlay.web.tsx +33 -0
  183. package/src/TrueSheetPeek.web.tsx +25 -23
  184. package/src/detentBackgrounds.ts +24 -0
  185. package/src/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  186. package/src/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  187. package/src/fabric/TrueSheetViewNativeComponent.ts +15 -10
  188. package/src/index.ts +1 -0
  189. package/src/mocks/index.ts +17 -2
  190. package/src/mocks/navigationExpoRouter.ts +6 -5
  191. package/src/navigation/base-types.ts +20 -0
  192. package/src/navigation/createTrueSheetRouter.ts +5 -33
  193. package/src/navigation/expo-router/base-types.ts +18 -0
  194. package/src/navigation/expo-router/index.ts +28 -30
  195. package/src/navigation/expo-router/types.ts +116 -0
  196. package/src/navigation/index.ts +6 -7
  197. package/src/navigation/screen/useSheetScreenState.ts +2 -0
  198. package/src/navigation/types.ts +75 -22
  199. package/src/web/constants.ts +1 -1
  200. package/src/web/layout.ts +196 -0
  201. package/src/web/vaul/constants.ts +9 -0
  202. package/src/web/vaul/context.ts +1 -0
  203. package/src/web/vaul/helpers.ts +5 -0
  204. package/src/web/vaul/index.tsx +75 -5
  205. package/src/web/vaul/use-snap-points.ts +32 -15
@@ -18,11 +18,27 @@ 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?
24
33
  val keyboardInset: Int
25
34
  val topInset: Int
35
+
36
+ /**
37
+ * True while the keyboard holds the sheet up: the collapsed stop then sits at
38
+ * the keyboard-free first detent (the "floor") so a drag-down dismisses the
39
+ * keyboard before it can dismiss the sheet.
40
+ */
41
+ val hasKeyboardFloor: Boolean
26
42
  }
27
43
 
28
44
  /**
@@ -44,6 +60,15 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
44
60
  private val maxContentHeight: Int? get() = delegate?.maxContentHeight
45
61
  private val keyboardInset: Int get() = delegate?.keyboardInset ?: 0
46
62
  private val topInset: Int get() = delegate?.topInset ?: 0
63
+ private val hasKeyboardFloor: Boolean get() = delegate?.hasKeyboardFloor == true
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
47
72
 
48
73
  /**
49
74
  * Height for auto (-1.0) detents: content + header + footer height.
@@ -52,14 +77,14 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
52
77
  private val autoDetentHeight: Int
53
78
  get() = contentHeight +
54
79
  (if (delegate?.absoluteHeader == true) 0 else headerHeight) +
55
- (if (delegate?.absoluteFooter == true) 0 else footerHeight)
80
+ (if (footerInAuto) footerHeight else 0)
56
81
 
57
82
  /**
58
- * Bottom inset for auto detents. A relative footer owns the sheet's bottom
59
- * edge, so it absorbs the inset instead of adding it to the auto height.
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.
60
85
  */
61
86
  private val autoDetentBottomInset: Int
62
- get() = if (footerHeight > 0 && delegate?.absoluteFooter == false) 0 else contentBottomInset
87
+ get() = if (footerHeight > 0 && footerInAuto) 0 else contentBottomInset
63
88
 
64
89
  /**
65
90
  * Bottom inset for peek detents. An absolute footer counts toward the peek
@@ -97,11 +122,37 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
97
122
  (detent * screenHeight).toInt() + contentBottomInset
98
123
  }
99
124
 
100
- val height = if (includeKeyboard) baseHeight + keyboardInset else baseHeight
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
101
136
  val maxAllowedHeight = screenHeight + contentBottomInset
102
137
  return maxContentHeight?.let { minOf(height, it, maxAllowedHeight) } ?: minOf(height, maxAllowedHeight)
103
138
  }
104
139
 
140
+ fun getBackgroundIndex(sheetTop: Int, displayedIndex: Int): Int {
141
+ if (detents.isEmpty()) return -1
142
+
143
+ val height = getVisibleSheetHeight(sheetTop)
144
+ var nextIndex = displayedIndex.coerceIn(0, detents.size - 1)
145
+ var distance = kotlin.math.abs(height - getVisibleSheetHeight(getSheetTopForDetentIndex(nextIndex)))
146
+ for (index in detents.indices) {
147
+ val nextDistance = kotlin.math.abs(height - getVisibleSheetHeight(getSheetTopForDetentIndex(index)))
148
+ if (nextDistance < distance) {
149
+ nextIndex = index
150
+ distance = nextDistance
151
+ }
152
+ }
153
+ return nextIndex
154
+ }
155
+
105
156
  /**
106
157
  * Get the expected sheet top position for a detent index.
107
158
  */
@@ -110,12 +161,31 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
110
161
  RNLog.w(reactContext, "TrueSheet: Detent index ($index) is out of bounds (0..${detents.size - 1})")
111
162
  return realScreenHeight
112
163
  }
113
- // Clamp to the space the sheet can actually occupy — matching
114
- // setupSheetDetents. A keyboard-inflated detent height can exceed it,
115
- // placing the expected top above the screen while the real sheet stops
116
- // at the top inset.
164
+ // With a keyboard floor only the last detent stays keyboard-shifted — it's
165
+ // the keyboard-expanded resting position; the stops below it are keyboard-free
166
+ val isKeyboardFreeStop = hasKeyboardFloor && index != detents.size - 1
167
+ return getSheetTop(detents[index], includeKeyboard = !isKeyboardFreeStop)
168
+ }
169
+
170
+ /**
171
+ * Top of the lowest stop the sheet can rest at: the keyboard floor while it's
172
+ * active, else the first detent.
173
+ */
174
+ fun getLowestSheetTop(): Int = if (hasKeyboardFloor) getSheetTop(detents[0], includeKeyboard = false) else getSheetTopForDetentIndex(0)
175
+
176
+ /**
177
+ * Top of the half stop while the keyboard floor is active: the keyboard-free
178
+ * second detent, or the floor itself for a single detent.
179
+ */
180
+ fun getKeyboardHalfStopTop(): Int = getSheetTop(detents[minOf(1, detents.size - 1)], includeKeyboard = false)
181
+
182
+ // Clamp to the space the sheet can actually occupy — matching
183
+ // setupSheetDetents. A keyboard-inflated detent height can exceed it,
184
+ // placing the expected top above the screen while the real sheet stops
185
+ // at the top inset.
186
+ private fun getSheetTop(detent: Double, includeKeyboard: Boolean = true): Int {
117
187
  val maxAvailableHeight = realScreenHeight - topInset
118
- return realScreenHeight - minOf(getDetentHeight(detents[index]), maxAvailableHeight)
188
+ return realScreenHeight - minOf(getDetentHeight(detent, includeKeyboard), maxAvailableHeight)
119
189
  }
120
190
 
121
191
  /**
@@ -152,6 +222,9 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
152
222
  */
153
223
  fun getStateForDetentIndex(index: Int): Int {
154
224
  val stateMap = getDetentStateMap() ?: return BottomSheetBehavior.STATE_HIDDEN
225
+ // With a keyboard floor the keyboard-expanded last detent lives at the
226
+ // expanded stop — the half stop (which may share its index) is keyboard-free
227
+ if (hasKeyboardFloor && index == detents.size - 1) return BottomSheetBehavior.STATE_EXPANDED
155
228
  return stateMap.entries.find { it.value == index }?.key ?: BottomSheetBehavior.STATE_HIDDEN
156
229
  }
157
230
 
@@ -169,8 +242,12 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
169
242
  */
170
243
  private fun getDetentStateMap(): Map<Int, Int>? =
171
244
  when (detents.size) {
245
+ // With one detent halfExpandedRatio matches the peek height, so the
246
+ // behavior can settle HALF_EXPANDED at the same position (e.g. an upward
247
+ // drag release) — map it too, or the settle is unmappable
172
248
  1 -> mapOf(
173
249
  BottomSheetBehavior.STATE_COLLAPSED to 0,
250
+ BottomSheetBehavior.STATE_HALF_EXPANDED to 0,
174
251
  BottomSheetBehavior.STATE_EXPANDED to 0
175
252
  )
176
253
 
@@ -197,16 +274,18 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
197
274
  * Find which segment the position falls into for interpolation.
198
275
  * @return Triple(fromIndex, toIndex, progress) where progress is 0-1, or null if no detents
199
276
  */
200
- fun findSegmentForPosition(positionPx: Int): Triple<Int, Int, Float>? {
277
+ private fun findSegmentForPosition(positionPx: Int): Triple<Int, Int, Float>? {
201
278
  val count = detents.size
202
279
  if (count == 0) return null
203
280
 
204
- val firstPos = getSheetTopForDetentIndex(0)
281
+ // A single keyboard-shifted detent can rest down at its keyboard floor —
282
+ // that range is still index 0, not a dismissal
283
+ val floorPos = getLowestSheetTop()
205
284
 
206
- // Position is below first detent (sheet is being dragged down to dismiss)
207
- if (positionPx > firstPos) {
208
- val range = realScreenHeight - firstPos
209
- val progress = if (range > 0) (positionPx - firstPos).toFloat() / range else 0f
285
+ // Position is below the lowest stop (sheet is being dragged down to dismiss)
286
+ if (positionPx > floorPos) {
287
+ val range = realScreenHeight - floorPos
288
+ val progress = if (range > 0) (positionPx - floorPos).toFloat() / range else 0f
210
289
  return Triple(-1, 0, progress)
211
290
  }
212
291
 
@@ -238,37 +317,23 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
238
317
  }
239
318
 
240
319
  /**
241
- * Returns continuous index (e.g., 0.5 = halfway between detent 0 and 1).
320
+ * Returns interpolated continuous index (e.g., 0.5 = halfway between detent
321
+ * 0 and 1) and screen fraction for a position, sharing a single segment
322
+ * lookup — this runs on every position frame.
242
323
  */
243
- fun getInterpolatedIndexForPosition(positionPx: Int): Float {
244
- val count = detents.size
245
- if (count == 0) return -1f
246
-
247
- val segment = findSegmentForPosition(positionPx) ?: return 0f
248
- val (fromIndex, _, progress) = segment
249
-
250
- if (fromIndex == -1) return -progress
251
- return fromIndex + progress
252
- }
253
-
254
- /**
255
- * Returns interpolated screen fraction for position.
256
- */
257
- fun getInterpolatedDetentForPosition(positionPx: Int): Float {
258
- val count = detents.size
259
- if (count == 0) return 0f
324
+ fun getInterpolatedPositionInfo(positionPx: Int): Pair<Float, Float> {
325
+ if (detents.isEmpty()) return Pair(-1f, 0f)
260
326
 
261
- val segment = findSegmentForPosition(positionPx) ?: return getDetentValueForIndex(0)
262
- val (fromIndex, toIndex, progress) = segment
327
+ val (fromIndex, toIndex, progress) = findSegmentForPosition(positionPx)
328
+ ?: return Pair(0f, getDetentValueForIndex(0))
263
329
 
264
330
  if (fromIndex == -1) {
265
- val firstDetent = getDetentValueForIndex(0)
266
- return maxOf(0f, firstDetent * (1 - progress))
331
+ return Pair(-progress, maxOf(0f, getDetentValueForIndex(0) * (1 - progress)))
267
332
  }
268
333
 
269
334
  val fromDetent = getDetentValueForIndex(fromIndex)
270
335
  val toDetent = getDetentValueForIndex(toIndex)
271
- return fromDetent + progress * (toDetent - fromDetent)
336
+ return Pair(fromIndex + progress, fromDetent + progress * (toDetent - fromDetent))
272
337
  }
273
338
 
274
339
  companion object {
@@ -139,6 +139,9 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) :
139
139
 
140
140
  override fun onTouchEvent(event: MotionEvent): Boolean {
141
141
  if (blockGestures) {
142
+ if (event.actionMasked == MotionEvent.ACTION_DOWN) {
143
+ parent?.requestDisallowInterceptTouchEvent(true)
144
+ }
142
145
  // When dimmed, consume touch and trigger click on ACTION_UP
143
146
  if (event.action == MotionEvent.ACTION_UP) {
144
147
  performClick()
@@ -0,0 +1,137 @@
1
+ package com.lodev09.truesheet.core
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.view.MotionEvent
5
+ import android.view.View
6
+ import android.widget.FrameLayout
7
+ import com.facebook.react.uimanager.JSPointerDispatcher
8
+ import com.facebook.react.uimanager.JSTouchDispatcher
9
+ import com.facebook.react.uimanager.RootView
10
+ import com.facebook.react.uimanager.ThemedReactContext
11
+ import com.facebook.react.uimanager.TouchTargetHelper
12
+ import com.facebook.react.uimanager.events.EventDispatcher
13
+ import com.lodev09.truesheet.utils.TouchEventDeduper
14
+
15
+ interface TrueSheetOverlayRootViewDelegate {
16
+ val eventDispatcher: EventDispatcher?
17
+ fun overlayRootViewDidChangeSize(width: Int, height: Int)
18
+ }
19
+
20
+ /**
21
+ * Window-level view hosting an overlay's children, attached to the activity's
22
+ * content view above the sheet coordinators.
23
+ *
24
+ * Implements RootView to dispatch touches to JS. Touches that miss every child
25
+ * fall through to the views beneath.
26
+ */
27
+ @SuppressLint("ViewConstructor")
28
+ class TrueSheetOverlayRootView(private val reactContext: ThemedReactContext) :
29
+ FrameLayout(reactContext),
30
+ RootView {
31
+
32
+ var delegate: TrueSheetOverlayRootViewDelegate? = null
33
+
34
+ private val eventDispatcher: EventDispatcher?
35
+ get() = delegate?.eventDispatcher
36
+
37
+ private val jsTouchDispatcher = JSTouchDispatcher(this)
38
+ private val jsPointerDispatcher = JSPointerDispatcher(this)
39
+ private val touchDeduper = TouchEventDeduper()
40
+
41
+ // Whether the current touch stream landed on a child
42
+ private var isHandlingStream = false
43
+
44
+ init {
45
+ layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
46
+
47
+ // Draws and receives touches above the sheet coordinators regardless of add order
48
+ translationZ = OVERLAY_Z
49
+ }
50
+
51
+ // Children are positioned by Fabric — a FrameLayout pass would stretch them to match_parent
52
+ override fun onLayout(
53
+ changed: Boolean,
54
+ left: Int,
55
+ top: Int,
56
+ right: Int,
57
+ bottom: Int
58
+ ) {
59
+ }
60
+
61
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
62
+ super.onSizeChanged(w, h, oldw, oldh)
63
+ delegate?.overlayRootViewDidChangeSize(w, h)
64
+ }
65
+
66
+ override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
67
+ if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
68
+ // The path ends with this root, so a touch that misses every child resolves to it
69
+ val target = TouchTargetHelper
70
+ .findTargetPathAndCoordinatesForTouch(ev.x, ev.y, this, FloatArray(2))
71
+ .firstNotNullOfOrNull { it.getView() }
72
+ isHandlingStream = target != null && target !== this
73
+ }
74
+ if (!isHandlingStream) return false
75
+ return super.dispatchTouchEvent(ev)
76
+ }
77
+
78
+ // ==================== RootView Implementation ====================
79
+
80
+ override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
81
+ eventDispatcher?.let { dispatcher ->
82
+ if (touchDeduper.shouldDispatch(event)) {
83
+ jsTouchDispatcher.handleTouchEvent(event, dispatcher, reactContext)
84
+ }
85
+ jsPointerDispatcher.handleMotionEvent(event, dispatcher, true)
86
+ }
87
+ return super.onInterceptTouchEvent(event)
88
+ }
89
+
90
+ override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
91
+ // Mirrors ReactSurfaceView: keep receiving onInterceptTouchEvent so
92
+ // jsTouchDispatcher sees the gesture end, but forward the request up the tree.
93
+ parent?.requestDisallowInterceptTouchEvent(disallowIntercept)
94
+ }
95
+
96
+ @SuppressLint("ClickableViewAccessibility")
97
+ override fun onTouchEvent(event: MotionEvent): Boolean {
98
+ eventDispatcher?.let { dispatcher ->
99
+ if (touchDeduper.shouldDispatch(event)) {
100
+ jsTouchDispatcher.handleTouchEvent(event, dispatcher, reactContext)
101
+ }
102
+ jsPointerDispatcher.handleMotionEvent(event, dispatcher, false)
103
+ }
104
+ super.onTouchEvent(event)
105
+ return true
106
+ }
107
+
108
+ override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
109
+ eventDispatcher?.let { jsPointerDispatcher.handleMotionEvent(event, it, true) }
110
+ return super.onHoverEvent(event)
111
+ }
112
+
113
+ override fun onHoverEvent(event: MotionEvent): Boolean {
114
+ eventDispatcher?.let { jsPointerDispatcher.handleMotionEvent(event, it, false) }
115
+ return super.onHoverEvent(event)
116
+ }
117
+
118
+ override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
119
+ eventDispatcher?.let { dispatcher ->
120
+ jsTouchDispatcher.onChildStartedNativeGesture(ev, dispatcher)
121
+ jsPointerDispatcher.onChildStartedNativeGesture(childView, ev, dispatcher)
122
+ }
123
+ }
124
+
125
+ override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
126
+ eventDispatcher?.let { jsTouchDispatcher.onChildEndedNativeGesture(ev, it) }
127
+ jsPointerDispatcher.onChildEndedNativeGesture()
128
+ }
129
+
130
+ override fun handleException(t: Throwable) {
131
+ reactContext.reactApplicationContext.handleException(RuntimeException(t))
132
+ }
133
+
134
+ companion object {
135
+ private const val OVERLAY_Z = 1000f
136
+ }
137
+ }
@@ -93,6 +93,21 @@ class DidDismissEvent(surfaceId: Int, viewId: Int) : Event<DidDismissEvent>(surf
93
93
  }
94
94
  }
95
95
 
96
+ /**
97
+ * Fired when the user tries to dismiss a non-dismissible sheet
98
+ */
99
+ class DismissAttemptEvent(surfaceId: Int, viewId: Int) : Event<DismissAttemptEvent>(surfaceId, viewId) {
100
+
101
+ override fun getEventName(): String = EVENT_NAME
102
+
103
+ override fun getEventData(): WritableMap = Arguments.createMap()
104
+
105
+ companion object {
106
+ const val EVENT_NAME = "topDismissAttempt"
107
+ const val REGISTRATION_NAME = "onDismissAttempt"
108
+ }
109
+ }
110
+
96
111
  /**
97
112
  * Fired when the sheet visibility changes due to screen transitions
98
113
  */
@@ -3,6 +3,7 @@ package com.lodev09.truesheet.utils
3
3
  import android.content.Context
4
4
  import android.os.Build
5
5
  import android.view.View
6
+ import android.view.WindowInsets
6
7
  import android.view.inputmethod.InputMethodManager
7
8
  import androidx.core.view.ViewCompat
8
9
  import androidx.core.view.WindowInsetsAnimationCompat
@@ -19,7 +20,7 @@ object KeyboardUtils {
19
20
  return isKeyboardVisible(rootView)
20
21
  }
21
22
 
22
- private fun isKeyboardVisible(view: View): Boolean {
23
+ fun isKeyboardVisible(view: View): Boolean {
23
24
  val insets = ViewCompat.getRootWindowInsets(view) ?: return false
24
25
  return insets.isVisible(WindowInsetsCompat.Type.ime())
25
26
  }
@@ -36,10 +37,22 @@ object KeyboardUtils {
36
37
  * Dismisses the soft keyboard with an optional callback when the animation completes.
37
38
  */
38
39
  fun dismiss(view: View, onComplete: (() -> Unit)?) {
40
+ if (!isKeyboardVisible(view)) {
41
+ onComplete?.invoke()
42
+ return
43
+ }
44
+
39
45
  val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
40
46
  val focusedView = view.rootView.findFocus()
41
47
 
42
- if (focusedView == null || !isKeyboardVisible(view)) {
48
+ // A keyboard can stay up with nothing focused, e.g. a blur that lands after focus was restored,
49
+ // so hide it through the window instead of treating it as already hidden.
50
+ if (focusedView == null) {
51
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
52
+ view.windowInsetsController?.hide(WindowInsets.Type.ime())
53
+ } else {
54
+ imm?.hideSoftInputFromWindow(view.windowToken, 0)
55
+ }
43
56
  onComplete?.invoke()
44
57
  return
45
58
  }
@@ -104,18 +104,6 @@ object ScreenUtils {
104
104
  */
105
105
  fun getScreenHeight(reactContext: ReactContext): Int = reactContext.resources.displayMetrics.heightPixels
106
106
 
107
- /**
108
- * Get the location of a view in screen coordinates
109
- *
110
- * @param view The view to get screen location for
111
- * @return IntArray with [x, y] coordinates in screen space
112
- */
113
- fun getScreenLocation(view: View): IntArray {
114
- val location = IntArray(2)
115
- view.getLocationOnScreen(location)
116
- return location
117
- }
118
-
119
107
  /**
120
108
  * Returns true if the device is a phone in portrait orientation.
121
109
  */
@@ -2,8 +2,10 @@ package com.lodev09.truesheet.utils
2
2
 
3
3
  import android.view.View
4
4
  import android.view.ViewGroup
5
+ import android.view.ViewParent
5
6
  import android.widget.ScrollView
6
7
  import androidx.core.widget.NestedScrollView
8
+ import com.facebook.react.uimanager.ThemedReactContext
7
9
 
8
10
  fun View.isDescendantOf(ancestor: View): Boolean {
9
11
  if (!isAttachedToWindow) return false
@@ -15,16 +17,26 @@ fun View.isDescendantOf(ancestor: View): Boolean {
15
17
  return false
16
18
  }
17
19
 
18
- fun ViewGroup.smoothScrollBy(dx: Int, dy: Int) {
19
- when (this) {
20
- is ScrollView -> smoothScrollBy(dx, dy)
21
- is NestedScrollView -> smoothScrollBy(dx, dy)
22
- }
23
- }
24
-
25
20
  fun ViewGroup.smoothScrollTo(x: Int, y: Int) {
26
21
  when (this) {
27
22
  is ScrollView -> smoothScrollTo(x, y)
28
23
  is NestedScrollView -> smoothScrollTo(x, y)
29
24
  }
30
25
  }
26
+
27
+ /**
28
+ * The content view hosting this view — the activity's, or a Modal dialog's.
29
+ * Window-level views (sheet coordinators, overlays) attach here.
30
+ */
31
+ fun View.findRootContainerView(reactContext: ThemedReactContext): ViewGroup? {
32
+ var current: ViewParent? = parent
33
+
34
+ while (current != null) {
35
+ if (current is ViewGroup && current.id == android.R.id.content) {
36
+ return current
37
+ }
38
+ current = current.parent
39
+ }
40
+
41
+ return reactContext.currentActivity?.findViewById(android.R.id.content)
42
+ }
@@ -5,6 +5,7 @@
5
5
  #include <jsi/jsi.h>
6
6
  #include <react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h>
7
7
  #include <react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h>
8
+ #include <react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewComponentDescriptor.h>
8
9
  #include <react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h>
9
10
 
10
11
  namespace facebook {
@@ -11,14 +11,6 @@ namespace facebook::react {
11
11
  class TrueSheetContentViewComponentDescriptor final
12
12
  : public ConcreteComponentDescriptor<TrueSheetContentViewShadowNode> {
13
13
  using ConcreteComponentDescriptor::ConcreteComponentDescriptor;
14
-
15
- void adopt(ShadowNode &shadowNode) const override {
16
- auto &concreteShadowNode =
17
- static_cast<TrueSheetContentViewShadowNode &>(shadowNode);
18
- concreteShadowNode.adjustLayoutWithState();
19
-
20
- ConcreteComponentDescriptor::adopt(shadowNode);
21
- }
22
14
  };
23
15
 
24
- } // namespace facebook::react
16
+ } // namespace facebook::react