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

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 (203) 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 +302 -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 +119 -50
  12. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +360 -163
  13. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +54 -12
  14. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +22 -9
  15. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +84 -5
  16. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +104 -39
  17. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +3 -0
  18. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetOverlayRootView.kt +137 -0
  19. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +15 -0
  20. package/android/src/main/java/com/lodev09/truesheet/utils/KeyboardUtils.kt +15 -2
  21. package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +0 -12
  22. package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +19 -7
  23. package/android/src/main/jni/TrueSheetSpec.h +1 -0
  24. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +1 -9
  25. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +110 -30
  26. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +30 -7
  27. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +2 -2
  28. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +9 -8
  29. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h +3 -5
  30. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +9 -3
  31. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +3 -6
  32. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +1 -1
  33. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +4 -6
  34. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.cpp +1 -1
  35. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.h +1 -1
  36. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetLayoutUtils.h +43 -0
  37. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewComponentDescriptor.h +21 -0
  38. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.cpp +17 -0
  39. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.h +33 -0
  40. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h +3 -5
  41. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +7 -40
  42. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.h +3 -7
  43. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.cpp +3 -5
  44. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.h +4 -5
  45. package/ios/TrueSheetContainerView.h +27 -7
  46. package/ios/TrueSheetContainerView.mm +31 -11
  47. package/ios/TrueSheetContentView.h +43 -15
  48. package/ios/TrueSheetContentView.mm +211 -189
  49. package/ios/TrueSheetFooterView.h +6 -0
  50. package/ios/TrueSheetFooterView.mm +93 -17
  51. package/ios/TrueSheetModule.h +2 -7
  52. package/ios/TrueSheetModule.mm +7 -16
  53. package/ios/TrueSheetOverlayView.h +32 -0
  54. package/ios/TrueSheetOverlayView.mm +161 -0
  55. package/ios/TrueSheetView.mm +212 -55
  56. package/ios/TrueSheetViewController.h +31 -3
  57. package/ios/TrueSheetViewController.mm +393 -94
  58. package/ios/core/TrueSheetBlurView.h +0 -2
  59. package/ios/core/TrueSheetBlurView.mm +3 -36
  60. package/ios/core/TrueSheetDetentCalculator.h +7 -1
  61. package/ios/core/TrueSheetDetentCalculator.mm +27 -4
  62. package/ios/core/TrueSheetGrabberView.mm +13 -3
  63. package/ios/core/TrueSheetKeyboardObserver.mm +2 -0
  64. package/ios/core/TrueSheetOverlayContainerView.h +31 -0
  65. package/ios/core/TrueSheetOverlayContainerView.mm +35 -0
  66. package/ios/events/TrueSheetLifecycleEvents.h +2 -0
  67. package/ios/events/TrueSheetLifecycleEvents.mm +8 -0
  68. package/ios/tvos/TrueSheetStubs.mm +12 -0
  69. package/ios/utils/UIView+ScrollEdgeInteraction.h +1 -0
  70. package/ios/utils/UIView+ScrollEdgeInteraction.mm +24 -2
  71. package/lib/module/TrueSheet.js +173 -39
  72. package/lib/module/TrueSheet.js.map +1 -1
  73. package/lib/module/TrueSheet.web.js +218 -243
  74. package/lib/module/TrueSheet.web.js.map +1 -1
  75. package/lib/module/TrueSheetBackground.js +15 -0
  76. package/lib/module/TrueSheetBackground.js.map +1 -0
  77. package/lib/module/TrueSheetOverlay.js +30 -0
  78. package/lib/module/TrueSheetOverlay.js.map +1 -0
  79. package/lib/module/TrueSheetOverlay.web.js +35 -0
  80. package/lib/module/TrueSheetOverlay.web.js.map +1 -0
  81. package/lib/module/TrueSheetPeek.web.js +20 -15
  82. package/lib/module/TrueSheetPeek.web.js.map +1 -1
  83. package/lib/module/detentBackgrounds.js +21 -0
  84. package/lib/module/detentBackgrounds.js.map +1 -0
  85. package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  86. package/lib/module/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  87. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +14 -10
  88. package/lib/module/index.js +1 -0
  89. package/lib/module/index.js.map +1 -1
  90. package/lib/module/mocks/index.js +18 -2
  91. package/lib/module/mocks/index.js.map +1 -1
  92. package/lib/module/mocks/navigationExpoRouter.js.map +1 -1
  93. package/lib/module/navigation/base-types.js +2 -0
  94. package/lib/module/navigation/base-types.js.map +1 -0
  95. package/lib/module/navigation/createTrueSheetRouter.js +0 -9
  96. package/lib/module/navigation/createTrueSheetRouter.js.map +1 -1
  97. package/lib/module/navigation/expo-router/base-types.js +2 -0
  98. package/lib/module/navigation/expo-router/base-types.js.map +1 -0
  99. package/lib/module/navigation/expo-router/index.js +10 -7
  100. package/lib/module/navigation/expo-router/index.js.map +1 -1
  101. package/lib/module/navigation/expo-router/types.js +4 -0
  102. package/lib/module/navigation/expo-router/types.js.map +1 -0
  103. package/lib/module/navigation/index.js.map +1 -1
  104. package/lib/module/navigation/screen/useSheetScreenState.js +1 -0
  105. package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -1
  106. package/lib/module/web/constants.js +1 -1
  107. package/lib/module/web/constants.js.map +1 -1
  108. package/lib/module/web/layout.js +168 -0
  109. package/lib/module/web/layout.js.map +1 -0
  110. package/lib/module/web/vaul/constants.js +4 -0
  111. package/lib/module/web/vaul/constants.js.map +1 -1
  112. package/lib/module/web/vaul/context.js.map +1 -1
  113. package/lib/module/web/vaul/helpers.js +1 -0
  114. package/lib/module/web/vaul/helpers.js.map +1 -1
  115. package/lib/module/web/vaul/index.js +44 -6
  116. package/lib/module/web/vaul/index.js.map +1 -1
  117. package/lib/module/web/vaul/use-snap-points.js +6 -8
  118. package/lib/module/web/vaul/use-snap-points.js.map +1 -1
  119. package/lib/typescript/src/TrueSheet.d.ts +6 -0
  120. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  121. package/lib/typescript/src/TrueSheet.types.d.ts +135 -34
  122. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  123. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  124. package/lib/typescript/src/TrueSheetBackground.d.ts +3 -0
  125. package/lib/typescript/src/TrueSheetBackground.d.ts.map +1 -0
  126. package/lib/typescript/src/TrueSheetOverlay.d.ts +8 -0
  127. package/lib/typescript/src/TrueSheetOverlay.d.ts.map +1 -0
  128. package/lib/typescript/src/TrueSheetOverlay.web.d.ts +8 -0
  129. package/lib/typescript/src/TrueSheetOverlay.web.d.ts.map +1 -0
  130. package/lib/typescript/src/TrueSheetPeek.web.d.ts +8 -4
  131. package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
  132. package/lib/typescript/src/detentBackgrounds.d.ts +10 -0
  133. package/lib/typescript/src/detentBackgrounds.d.ts.map +1 -0
  134. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts +6 -0
  135. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts.map +1 -0
  136. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +11 -7
  137. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  138. package/lib/typescript/src/index.d.ts +1 -0
  139. package/lib/typescript/src/index.d.ts.map +1 -1
  140. package/lib/typescript/src/mocks/index.d.ts +4 -0
  141. package/lib/typescript/src/mocks/index.d.ts.map +1 -1
  142. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +3 -3
  143. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -1
  144. package/lib/typescript/src/navigation/base-types.d.ts +9 -0
  145. package/lib/typescript/src/navigation/base-types.d.ts.map +1 -0
  146. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +2 -13
  147. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -1
  148. package/lib/typescript/src/navigation/expo-router/base-types.d.ts +8 -0
  149. package/lib/typescript/src/navigation/expo-router/base-types.d.ts.map +1 -0
  150. package/lib/typescript/src/navigation/expo-router/index.d.ts +8 -6
  151. package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -1
  152. package/lib/typescript/src/navigation/expo-router/types.d.ts +44 -0
  153. package/lib/typescript/src/navigation/expo-router/types.d.ts.map +1 -0
  154. package/lib/typescript/src/navigation/index.d.ts +3 -4
  155. package/lib/typescript/src/navigation/index.d.ts.map +1 -1
  156. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +2 -1
  157. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -1
  158. package/lib/typescript/src/navigation/types.d.ts +41 -5
  159. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  160. package/lib/typescript/src/web/constants.d.ts +1 -1
  161. package/lib/typescript/src/web/constants.d.ts.map +1 -1
  162. package/lib/typescript/src/web/layout.d.ts +19 -0
  163. package/lib/typescript/src/web/layout.d.ts.map +1 -0
  164. package/lib/typescript/src/web/vaul/constants.d.ts +1 -0
  165. package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -1
  166. package/lib/typescript/src/web/vaul/context.d.ts +1 -0
  167. package/lib/typescript/src/web/vaul/context.d.ts.map +1 -1
  168. package/lib/typescript/src/web/vaul/helpers.d.ts +1 -0
  169. package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
  170. package/lib/typescript/src/web/vaul/index.d.ts +7 -1
  171. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
  172. package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
  173. package/package.json +17 -8
  174. package/react-native.config.js +1 -0
  175. package/src/TrueSheet.tsx +206 -48
  176. package/src/TrueSheet.types.ts +142 -35
  177. package/src/TrueSheet.web.tsx +270 -290
  178. package/src/TrueSheetBackground.tsx +17 -0
  179. package/src/TrueSheetOverlay.tsx +24 -0
  180. package/src/TrueSheetOverlay.web.tsx +33 -0
  181. package/src/TrueSheetPeek.web.tsx +25 -23
  182. package/src/detentBackgrounds.ts +24 -0
  183. package/src/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  184. package/src/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  185. package/src/fabric/TrueSheetViewNativeComponent.ts +14 -10
  186. package/src/index.ts +1 -0
  187. package/src/mocks/index.ts +17 -2
  188. package/src/mocks/navigationExpoRouter.ts +6 -5
  189. package/src/navigation/base-types.ts +20 -0
  190. package/src/navigation/createTrueSheetRouter.ts +5 -33
  191. package/src/navigation/expo-router/base-types.ts +18 -0
  192. package/src/navigation/expo-router/index.ts +28 -30
  193. package/src/navigation/expo-router/types.ts +116 -0
  194. package/src/navigation/index.ts +6 -7
  195. package/src/navigation/screen/useSheetScreenState.ts +2 -0
  196. package/src/navigation/types.ts +75 -22
  197. package/src/web/constants.ts +1 -1
  198. package/src/web/layout.ts +196 -0
  199. package/src/web/vaul/constants.ts +4 -0
  200. package/src/web/vaul/context.ts +1 -0
  201. package/src/web/vaul/helpers.ts +5 -0
  202. package/src/web/vaul/index.tsx +67 -5
  203. package/src/web/vaul/use-snap-points.ts +6 -14
@@ -1,28 +1,18 @@
1
1
  package com.lodev09.truesheet
2
2
 
3
3
  import android.annotation.SuppressLint
4
- import android.view.MotionEvent
5
- import android.view.View
6
- import android.view.ViewParent
7
4
  import com.facebook.react.bridge.WritableNativeMap
8
- import com.facebook.react.uimanager.JSPointerDispatcher
9
- import com.facebook.react.uimanager.JSTouchDispatcher
10
5
  import com.facebook.react.uimanager.PixelUtil.pxToDp
11
- import com.facebook.react.uimanager.PointerEvents
12
- import com.facebook.react.uimanager.RootView
13
6
  import com.facebook.react.uimanager.StateWrapper
14
7
  import com.facebook.react.uimanager.ThemedReactContext
15
- import com.facebook.react.uimanager.events.EventDispatcher
16
8
  import com.facebook.react.views.view.ReactViewGroup
17
- import com.lodev09.truesheet.core.TrueSheetCoordinatorLayout
18
- import com.lodev09.truesheet.utils.TouchEventDeduper
19
9
 
20
10
  /**
21
- * Delegate interface for footer view size changes and event dispatching
11
+ * Delegate interface for footer view size changes
22
12
  */
23
13
  interface TrueSheetFooterViewDelegate {
24
14
  fun footerViewDidChangeSize(width: Int, height: Int)
25
- val eventDispatcher: EventDispatcher?
15
+ fun footerViewDidLayout()
26
16
  }
27
17
 
28
18
  /**
@@ -30,40 +20,29 @@ interface TrueSheetFooterViewDelegate {
30
20
  * This is the second child of TrueSheetContainerView
31
21
  * Positioned absolutely at the bottom of the sheet
32
22
  *
33
- * Implements RootView to handle touch events when positioned outside parent bounds.
23
+ * Touches reach it through the controller's RootView like any other sheet
24
+ * content — the container tracks the sheet's visible height, so the footer
25
+ * always lays out inside the container's bounds.
34
26
  */
35
27
  @SuppressLint("ViewConstructor")
36
- class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
37
- ReactViewGroup(reactContext),
38
- RootView {
28
+ class TrueSheetFooterView(reactContext: ThemedReactContext) : ReactViewGroup(reactContext) {
39
29
 
40
30
  var delegate: TrueSheetFooterViewDelegate? = null
41
31
  var stateWrapper: StateWrapper? = null
42
32
 
43
- private val eventDispatcher: EventDispatcher?
44
- get() = delegate?.eventDispatcher
45
-
46
33
  private var lastWidth = 0
47
34
  private var lastHeight = 0
48
- private var bottomInset = 0
49
35
 
50
- /**
51
- * Skips the inset while the keyboard is open — an absolute footer rises
52
- * above the keyboard, so the inset would leave a gap.
53
- */
54
- var keyboardVisible = false
55
- set(value) {
56
- if (field == value) return
57
- field = value
58
- pushBottomInsetState()
59
- }
36
+ var bottomInset = 0
37
+ private set
60
38
 
61
39
  /**
62
40
  * Height the footer occupies above the keyboard — its layout height minus
63
- * the safe-area inset it drops while the keyboard is open.
41
+ * the safe-area inset, which rides below the keyboard's top edge while the
42
+ * keyboard is open (see positionFooter).
64
43
  */
65
44
  val keyboardOcclusionHeight: Int
66
- get() = maxOf(0, height - if (keyboardVisible) 0 else bottomInset)
45
+ get() = maxOf(0, height - bottomInset)
67
46
 
68
47
  /**
69
48
  * Tells the shadow node to pad the footer's bottom edge with the sheet's
@@ -73,12 +52,9 @@ class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
73
52
  fun setBottomInset(inset: Int) {
74
53
  if (bottomInset == inset) return
75
54
  bottomInset = inset
76
- pushBottomInsetState()
77
- }
78
55
 
79
- private fun pushBottomInsetState() {
80
56
  val sw = stateWrapper ?: return
81
- val insetDp = (if (keyboardVisible) 0 else bottomInset).toFloat().pxToDp()
57
+ val insetDp = bottomInset.toFloat().pxToDp()
82
58
 
83
59
  // Synchronous update — the footer must be padded before detents are
84
60
  // configured, otherwise the auto detent is set up an inset short
@@ -90,14 +66,6 @@ class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
90
66
  sw.updateState(newState)
91
67
  }
92
68
 
93
- private val jsTouchDispatcher = JSTouchDispatcher(this)
94
- private var jsPointerDispatcher: JSPointerDispatcher? = null
95
- private val touchDeduper = TouchEventDeduper()
96
-
97
- init {
98
- jsPointerDispatcher = JSPointerDispatcher(this)
99
- }
100
-
101
69
  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
102
70
  super.onSizeChanged(w, h, oldw, oldh)
103
71
 
@@ -108,72 +76,15 @@ class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
108
76
  }
109
77
  }
110
78
 
111
- // ==================== RootView Implementation ====================
112
-
113
- override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
114
- eventDispatcher?.let { dispatcher ->
115
- if (touchDeduper.shouldDispatch(event)) {
116
- jsTouchDispatcher.handleTouchEvent(event, dispatcher, reactContext)
117
- }
118
- jsPointerDispatcher?.handleMotionEvent(event, dispatcher, true)
119
- }
120
- return super.onInterceptTouchEvent(event)
121
- }
122
-
123
- override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
124
- // Mirrors ReactSurfaceView: keep receiving onInterceptTouchEvent so
125
- // jsTouchDispatcher sees the gesture end, but forward the request up the tree.
126
- parent?.requestDisallowInterceptTouchEvent(disallowIntercept)
127
- }
128
-
129
- override fun onTouchEvent(event: MotionEvent): Boolean {
130
- if (pointerEvents == PointerEvents.NONE || pointerEvents == PointerEvents.BOX_NONE) {
131
- return false
132
- }
133
-
134
- eventDispatcher?.let { dispatcher ->
135
- if (touchDeduper.shouldDispatch(event)) {
136
- jsTouchDispatcher.handleTouchEvent(event, dispatcher, reactContext)
137
- }
138
- jsPointerDispatcher?.handleMotionEvent(event, dispatcher, false)
139
- }
140
- super.onTouchEvent(event)
141
- return true
142
- }
143
-
144
- override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
145
- eventDispatcher?.let { jsPointerDispatcher?.handleMotionEvent(event, it, true) }
146
- return super.onHoverEvent(event)
147
- }
148
-
149
- override fun onHoverEvent(event: MotionEvent): Boolean {
150
- eventDispatcher?.let { jsPointerDispatcher?.handleMotionEvent(event, it, false) }
151
- return super.onHoverEvent(event)
152
- }
153
-
154
- override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
155
- findCoordinatorLayout()?.childDidClaimNativeGesture()
156
- eventDispatcher?.let { dispatcher ->
157
- jsTouchDispatcher.onChildStartedNativeGesture(ev, dispatcher)
158
- jsPointerDispatcher?.onChildStartedNativeGesture(childView, ev, dispatcher)
159
- }
160
- }
161
-
162
- private fun findCoordinatorLayout(): TrueSheetCoordinatorLayout? {
163
- var current: ViewParent? = parent
164
- while (current != null && current !is TrueSheetCoordinatorLayout) {
165
- current = current.parent
166
- }
167
- return current as? TrueSheetCoordinatorLayout
168
- }
169
-
170
- override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
171
- eventDispatcher?.let { jsTouchDispatcher.onChildEndedNativeGesture(ev, it) }
172
- jsPointerDispatcher?.onChildEndedNativeGesture()
173
- }
174
-
175
- override fun handleException(t: Throwable) {
176
- reactContext.reactApplicationContext.handleException(RuntimeException(t))
79
+ override fun onLayout(
80
+ changed: Boolean,
81
+ left: Int,
82
+ top: Int,
83
+ right: Int,
84
+ bottom: Int
85
+ ) {
86
+ super.onLayout(changed, left, top, right, bottom)
87
+ delegate?.footerViewDidLayout()
177
88
  }
178
89
 
179
90
  companion object {
@@ -0,0 +1,116 @@
1
+ package com.lodev09.truesheet
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.view.View
5
+ import android.view.ViewGroup
6
+ import android.view.accessibility.AccessibilityEvent
7
+ import com.facebook.react.uimanager.StateWrapper
8
+ import com.facebook.react.uimanager.ThemedReactContext
9
+ import com.facebook.react.uimanager.events.EventDispatcher
10
+ import com.facebook.react.views.view.ReactViewGroup
11
+ import com.lodev09.truesheet.core.TrueSheetOverlayRootView
12
+ import com.lodev09.truesheet.core.TrueSheetOverlayRootViewDelegate
13
+ import com.lodev09.truesheet.utils.findRootContainerView
14
+
15
+ /**
16
+ * Hidden host that renders its children in a root view attached to the
17
+ * activity's content view, above every presented sheet.
18
+ */
19
+ @SuppressLint("ViewConstructor")
20
+ class TrueSheetOverlayView(private val reactContext: ThemedReactContext) :
21
+ ReactViewGroup(reactContext),
22
+ TrueSheetOverlayRootViewDelegate {
23
+
24
+ private val rootView = TrueSheetOverlayRootView(reactContext).apply {
25
+ delegate = this@TrueSheetOverlayView
26
+ }
27
+
28
+ private var rootContainerView: ViewGroup? = null
29
+
30
+ override var eventDispatcher: EventDispatcher? = null
31
+
32
+ private var lastWidth = 0
33
+ private var lastHeight = 0
34
+
35
+ // Fabric hands back a new StateWrapper on every state commit — including our
36
+ // own pushes — so pushState dedupes against the last pushed size to break the loop
37
+ private var lastStateWidth = 0
38
+ private var lastStateHeight = 0
39
+
40
+ var stateWrapper: StateWrapper? = null
41
+ set(value) {
42
+ field = value
43
+ pushState()
44
+ }
45
+
46
+ init {
47
+ // The host stays in the React tree for layout only — children render in the root view
48
+ visibility = GONE
49
+ }
50
+
51
+ // ==================== View Hierarchy Management ====================
52
+
53
+ override fun onAttachedToWindow() {
54
+ super.onAttachedToWindow()
55
+ attachRootView()
56
+ }
57
+
58
+ override fun onDetachedFromWindow() {
59
+ detachRootView()
60
+ super.onDetachedFromWindow()
61
+ }
62
+
63
+ private fun attachRootView() {
64
+ if (rootView.parent != null) return
65
+
66
+ val container = findRootContainerView(reactContext) ?: return
67
+ container.addView(rootView)
68
+ rootContainerView = container
69
+ }
70
+
71
+ private fun detachRootView() {
72
+ rootContainerView?.removeView(rootView)
73
+ rootContainerView = null
74
+ }
75
+
76
+ fun onDropInstance() {
77
+ detachRootView()
78
+ rootView.delegate = null
79
+ }
80
+
81
+ override fun addView(child: View?, index: Int) {
82
+ rootView.addView(child, index)
83
+ }
84
+
85
+ override fun getChildCount(): Int = rootView.childCount
86
+
87
+ override fun getChildAt(index: Int): View? = rootView.getChildAt(index)
88
+
89
+ override fun removeViewAt(index: Int) {
90
+ rootView.removeViewAt(index)
91
+ }
92
+
93
+ // Accessibility: children live in the root view since this view is hidden
94
+ override fun addChildrenForAccessibility(outChildren: ArrayList<View>) {}
95
+ override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent): Boolean = false
96
+
97
+ // ==================== State ====================
98
+
99
+ override fun overlayRootViewDidChangeSize(width: Int, height: Int) {
100
+ if (width == lastWidth && height == lastHeight) return
101
+
102
+ lastWidth = width
103
+ lastHeight = height
104
+ pushState()
105
+ }
106
+
107
+ private fun pushState() {
108
+ if (lastWidth == 0 && lastHeight == 0) return
109
+ if (lastWidth == lastStateWidth && lastHeight == lastStateHeight) return
110
+ val sw = stateWrapper ?: return
111
+
112
+ lastStateWidth = lastWidth
113
+ lastStateHeight = lastHeight
114
+ TrueSheetStateUpdater.updateContainerSize(sw, lastWidth, lastHeight)
115
+ }
116
+ }
@@ -0,0 +1,45 @@
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.ReactStylesDiffMap
6
+ import com.facebook.react.uimanager.StateWrapper
7
+ import com.facebook.react.uimanager.ThemedReactContext
8
+ import com.facebook.react.uimanager.UIManagerHelper
9
+ import com.facebook.react.uimanager.ViewGroupManager
10
+ import com.facebook.react.uimanager.annotations.ReactProp
11
+
12
+ /**
13
+ * ViewManager for TrueSheetOverlayView
14
+ * Renders content above every presented sheet
15
+ */
16
+ @ReactModule(name = TrueSheetOverlayViewManager.REACT_CLASS)
17
+ class TrueSheetOverlayViewManager : ViewGroupManager<TrueSheetOverlayView>() {
18
+
19
+ override fun getName(): String = REACT_CLASS
20
+
21
+ override fun createViewInstance(reactContext: ThemedReactContext): TrueSheetOverlayView = TrueSheetOverlayView(reactContext)
22
+
23
+ override fun onDropViewInstance(view: TrueSheetOverlayView) {
24
+ super.onDropViewInstance(view)
25
+ view.onDropInstance()
26
+ }
27
+
28
+ override fun addEventEmitters(reactContext: ThemedReactContext, view: TrueSheetOverlayView) {
29
+ view.eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
30
+ }
31
+
32
+ override fun updateState(view: TrueSheetOverlayView, props: ReactStylesDiffMap?, stateWrapper: StateWrapper?): Any? {
33
+ view.stateWrapper = stateWrapper
34
+ return null
35
+ }
36
+
37
+ @ReactProp(name = "pointerEvents")
38
+ fun setPointerEvents(view: TrueSheetOverlayView, pointerEventsStr: String?) {
39
+ view.pointerEvents = PointerEvents.parsePointerEvents(pointerEventsStr)
40
+ }
41
+
42
+ companion object {
43
+ const val REACT_CLASS = "TrueSheetOverlayView"
44
+ }
45
+ }
@@ -40,6 +40,7 @@ class TrueSheetPackage : BaseReactPackage() {
40
40
  TrueSheetContentViewManager(),
41
41
  TrueSheetHeaderViewManager(),
42
42
  TrueSheetFooterViewManager(),
43
- TrueSheetPeekViewManager()
43
+ TrueSheetPeekViewManager(),
44
+ TrueSheetOverlayViewManager()
44
45
  )
45
46
  }
@@ -1,5 +1,7 @@
1
1
  package com.lodev09.truesheet
2
2
 
3
+ import com.facebook.react.bridge.WritableNativeMap
4
+ import com.facebook.react.uimanager.PixelUtil.pxToDp
3
5
  import com.facebook.react.uimanager.StateWrapper
4
6
 
5
7
  /**
@@ -22,6 +24,22 @@ object TrueSheetStateUpdater {
22
24
  fun updateState(stateWrapper: StateWrapper, widthDp: Float, heightDp: Float): Boolean =
23
25
  isAvailable && nativeUpdateState(stateWrapper, widthDp, heightDp)
24
26
 
27
+ /**
28
+ * Pushes the container size (px) into the Fabric state — synchronously when
29
+ * the bridge is available, async otherwise.
30
+ */
31
+ fun updateContainerSize(stateWrapper: StateWrapper, widthPx: Int, heightPx: Int) {
32
+ val widthDp = widthPx.toFloat().pxToDp()
33
+ val heightDp = heightPx.toFloat().pxToDp()
34
+
35
+ if (updateState(stateWrapper, widthDp, heightDp)) return
36
+
37
+ val newStateData = WritableNativeMap()
38
+ newStateData.putDouble("containerWidth", widthDp.toDouble())
39
+ newStateData.putDouble("containerHeight", heightDp.toDouble())
40
+ stateWrapper.updateState(newStateData)
41
+ }
42
+
25
43
  /** Returns false when unavailable — caller should fall back to async updateState. */
26
44
  fun updateFooterState(stateWrapper: StateWrapper, bottomInsetDp: Float): Boolean =
27
45
  isAvailable && nativeUpdateFooterState(stateWrapper, bottomInsetDp)
@@ -6,7 +6,6 @@ import android.view.ViewGroup
6
6
  import android.view.accessibility.AccessibilityEvent
7
7
  import androidx.annotation.UiThread
8
8
  import com.facebook.react.bridge.LifecycleEventListener
9
- import com.facebook.react.bridge.WritableNativeMap
10
9
  import com.facebook.react.uimanager.PixelUtil.dpToPx
11
10
  import com.facebook.react.uimanager.PixelUtil.pxToDp
12
11
  import com.facebook.react.uimanager.StateWrapper
@@ -22,6 +21,7 @@ import com.lodev09.truesheet.core.RNScreensEventObserverDelegate
22
21
  import com.lodev09.truesheet.core.TrueSheetStackManager
23
22
  import com.lodev09.truesheet.events.*
24
23
  import com.lodev09.truesheet.utils.KeyboardUtils
24
+ import com.lodev09.truesheet.utils.findRootContainerView
25
25
 
26
26
  /**
27
27
  * Main TrueSheet host view that manages the sheet and dispatches events to JavaScript.
@@ -101,13 +101,30 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
101
101
 
102
102
  override fun onAttachedToWindow() {
103
103
  super.onAttachedToWindow()
104
+ presentInitialIfNeeded()
105
+ }
104
106
 
105
- if (initialDetentIndex >= 0 && !didInitiallyPresent) {
106
- didInitiallyPresent = true
107
- if (initialDetentAnimated) {
108
- present(initialDetentIndex, true) { }
109
- } else {
110
- post { present(initialDetentIndex, false) { } }
107
+ /**
108
+ * JS holds initialDetentIndex back one commit so effect-driven content (e.g. a
109
+ * navigation footer set via setOptions) commits together with it. The prop then
110
+ * arrives after attach, so the ViewManager triggers this on prop update too.
111
+ * Posted so sibling mounts from the current transaction land before measuring.
112
+ */
113
+ fun presentInitialIfNeeded() {
114
+ if (initialDetentIndex < 0 || didInitiallyPresent || !isAttachedToWindow) return
115
+
116
+ didInitiallyPresent = true
117
+ post {
118
+ // The view may have been dropped (flag reset) or detached before this
119
+ // ran — roll back so a later attach retries instead of presenting a
120
+ // sheet with no live host.
121
+ if (!didInitiallyPresent) return@post
122
+ if (!isAttachedToWindow) {
123
+ didInitiallyPresent = false
124
+ return@post
125
+ }
126
+ if (!viewController.isPresented) {
127
+ present(initialDetentIndex, initialDetentAnimated) { }
111
128
  }
112
129
  }
113
130
  }
@@ -187,6 +204,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
187
204
  setupScrollable()
188
205
 
189
206
  if (viewController.isPresented) {
207
+ viewController.setupBackground()
190
208
  viewController.sheetView?.setupBackground()
191
209
  viewController.sheetView?.setupGrabber()
192
210
  viewController.sheetView?.updateGravity()
@@ -207,15 +225,15 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
207
225
  viewController.maxContentWidth = width
208
226
  }
209
227
 
210
- fun setAnchor(anchor: String?) {
211
- val value = TrueSheetAnchor.fromString(anchor)
212
- if (viewController.anchor == value) return
213
- viewController.anchor = value
228
+ fun setPlacement(placement: String?) {
229
+ val value = TrueSheetPlacement.fromString(placement)
230
+ if (viewController.placement == value) return
231
+ viewController.placement = value
214
232
  }
215
233
 
216
- fun setAnchorOffset(offset: Int) {
217
- if (viewController.anchorOffset == offset) return
218
- viewController.anchorOffset = offset
234
+ fun setPlacementOffset(offset: Int) {
235
+ if (viewController.placementOffset == offset) return
236
+ viewController.placementOffset = offset
219
237
  }
220
238
 
221
239
  fun setDimmed(dimmed: Boolean) {
@@ -243,9 +261,12 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
243
261
  viewController.sheetCornerRadius = radius
244
262
  }
245
263
 
264
+ fun setDetentBackgrounds(colors: List<Int?>) {
265
+ viewController.detentBackgrounds = colors
266
+ }
267
+
246
268
  fun setSheetBackgroundColor(color: Int?) {
247
- if (viewController.sheetBackgroundColor == color) return
248
- viewController.sheetBackgroundColor = color
269
+ viewController.backgroundColor = color
249
270
  }
250
271
 
251
272
  fun setDismissible(dismissible: Boolean) {
@@ -287,6 +308,12 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
287
308
  setupScrollable()
288
309
  }
289
310
 
311
+ fun setScrollableHandle(handle: Int) {
312
+ if (viewController.scrollableHandle == handle) return
313
+ viewController.scrollableHandle = handle
314
+ setupScrollable()
315
+ }
316
+
290
317
  fun setAbsoluteHeader(absolute: Boolean) {
291
318
  viewController.absoluteHeader = absolute
292
319
  }
@@ -300,14 +327,20 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
300
327
  viewController.positionFooter()
301
328
  }
302
329
 
330
+ fun setFooterAvoidsKeyboard(avoids: Boolean) {
331
+ viewController.footerAvoidsKeyboard = avoids
332
+ viewController.containerView?.footerAvoidsKeyboard = avoids
333
+ viewController.positionFooter()
334
+ }
335
+
303
336
  private fun setupScrollable() {
304
337
  viewController.containerView?.let {
305
- it.insetAdjustment = viewController.insetAdjustment
306
- it.scrollViewBottomInset = viewController.contentBottomInset
307
338
  it.scrollableOptions = viewController.scrollableOptions
308
- it.hasAutoDetent = viewController.detents.contains(-1.0)
339
+ it.scrollableHandle = viewController.scrollableHandle
340
+ it.scrollableBottomInset = viewController.contentBottomInset
309
341
  it.footerBottomInset = viewController.contentBottomInset
310
342
  it.absoluteFooter = viewController.absoluteFooter
343
+ it.footerAvoidsKeyboard = viewController.footerAvoidsKeyboard
311
344
  it.setupScrollable()
312
345
  }
313
346
 
@@ -354,18 +387,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
354
387
  lastContainerWidth = width
355
388
  lastContainerHeight = height
356
389
 
357
- val sw = stateWrapper ?: return
358
- val widthDp = width.toFloat().pxToDp()
359
- val heightDp = height.toFloat().pxToDp()
360
-
361
- // Synchronous update — commits and mounts within the same UI-thread frame
362
- if (TrueSheetStateUpdater.updateState(sw, widthDp, heightDp)) return
363
-
364
- // Fallback: async state update
365
- val newStateData = WritableNativeMap()
366
- newStateData.putDouble("containerWidth", widthDp.toDouble())
367
- newStateData.putDouble("containerHeight", heightDp.toDouble())
368
- sw.updateState(newStateData)
390
+ stateWrapper?.let { TrueSheetStateUpdater.updateContainerSize(it, width, height) }
369
391
  }
370
392
 
371
393
  // ==================== Sheet Actions ====================
@@ -385,11 +407,22 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
385
407
  val parentSheet = TrueSheetStackManager.getTopmostSheet()
386
408
  val isFocusedViewWithinSheet = parentSheet?.viewController?.isFocusedViewWithinSheet() == true
387
409
  val shouldDismissKeyboard = isFocusedViewWithinSheet || viewController.isDimmedAtDetentIndex(detentIndex)
388
- if (KeyboardUtils.isKeyboardVisible(reactContext) && shouldDismissKeyboard) {
410
+ if (shouldDismissKeyboard && KeyboardUtils.isKeyboardVisible(reactContext)) {
389
411
  viewController.saveFocusedView()
390
412
  KeyboardUtils.dismiss(this) {
391
- post { present(detentIndex, animated, promiseCallback) }
413
+ post { presentSheet(detentIndex, animated, promiseCallback) }
392
414
  }
415
+ // Blur like iOS so JS focus state stays in sync until restoreFocusedView() refocuses it
416
+ rootView.findFocus()?.clearFocus()
417
+ return
418
+ }
419
+
420
+ presentSheet(detentIndex, animated, promiseCallback)
421
+ }
422
+
423
+ private fun presentSheet(detentIndex: Int, animated: Boolean, promiseCallback: () -> Unit) {
424
+ if (viewController.isPresented) {
425
+ promiseCallback()
393
426
  return
394
427
  }
395
428
 
@@ -469,7 +502,13 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
469
502
  if (viewController.containerView == null) return@post
470
503
 
471
504
  viewController.setupSheetDetentsForSizeChange()
472
- TrueSheetStackManager.updateParentTranslation(this)
505
+ // While settling, onSlide drives the parent translation in realtime —
506
+ // an animated update here would race it frame-by-frame, staggering the
507
+ // emitted position. Keyboard-driven resizes leave the parent alone (like
508
+ // iOS); keyboardDidHide resizes again once the detents are keyboard-free.
509
+ if (!viewController.isSettling && !viewController.isKeyboardActive) {
510
+ TrueSheetStackManager.updateParentTranslation(this)
511
+ }
473
512
  }
474
513
  }
475
514
 
@@ -497,14 +536,46 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
497
536
  }
498
537
  }
499
538
 
539
+ /**
540
+ * Realtime counterpart of updateTranslationForChild, driven by the child's
541
+ * onSlide frames so stacked sheets move in lockstep instead of on
542
+ * independently-timed animations.
543
+ */
544
+ fun syncTranslationForChild(childSheetTop: Int, childMinSheetTop: Int) {
545
+ if (!viewController.isSheetVisible || viewController.isExpanded) return
546
+
547
+ val mySheetTop = viewController.detentCalculator.getSheetTopForDetentIndex(viewController.currentDetentIndex)
548
+ val targetTranslation = maxOf(0, childMinSheetTop - mySheetTop)
549
+ val actualTranslation = maxOf(0, childSheetTop - mySheetTop)
550
+ val currentTranslation = viewController.currentTranslationY
551
+
552
+ // Ride the child's actual position, but only between the current
553
+ // translation and the settled target — tracks the child frame-by-frame
554
+ // when the alignment changes (auto detent resize) while staying put when
555
+ // the child moves between detents above its minimum. Returning early on
556
+ // no-ops avoids cancelling an in-flight realign animation.
557
+ val newTranslation = actualTranslation.coerceIn(
558
+ minOf(currentTranslation, targetTranslation),
559
+ maxOf(currentTranslation, targetTranslation)
560
+ )
561
+ if (newTranslation == currentTranslation) return
562
+
563
+ viewController.translateSheet(newTranslation, animated = false)
564
+
565
+ val additionalTranslation = newTranslation - currentTranslation
566
+ if (additionalTranslation > 0) {
567
+ TrueSheetStackManager.getParentSheet(this)?.addTranslation(additionalTranslation, animated = false)
568
+ }
569
+ }
570
+
500
571
  /**
501
572
  * Recursively adds translation to this sheet and all parent sheets.
502
573
  */
503
- private fun addTranslation(amount: Int) {
574
+ private fun addTranslation(amount: Int, animated: Boolean = true) {
504
575
  if (viewController.isExpanded) return
505
576
 
506
- viewController.translateSheet(viewController.currentTranslationY + amount)
507
- TrueSheetStackManager.getParentSheet(this)?.addTranslation(amount)
577
+ viewController.translateSheet(viewController.currentTranslationY + amount, animated)
578
+ TrueSheetStackManager.getParentSheet(this)?.addTranslation(amount, animated)
508
579
  }
509
580
 
510
581
  /**
@@ -542,6 +613,11 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
542
613
  eventDispatcher?.dispatchEvent(WillDismissEvent(surfaceId, id))
543
614
  }
544
615
 
616
+ override fun viewControllerDidAttemptDismiss() {
617
+ val surfaceId = UIManagerHelper.getSurfaceId(this)
618
+ eventDispatcher?.dispatchEvent(DismissAttemptEvent(surfaceId, id))
619
+ }
620
+
545
621
  override fun viewControllerDidDismiss(parent: TrueSheetView?) {
546
622
  // Detach coordinator from the root container view
547
623
  viewController.coordinatorLayout?.let { rootContainerView?.removeView(it) }
@@ -626,7 +702,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
626
702
  viewController.commitKeyboardDetent()
627
703
  }
628
704
 
629
- // When the ScrollView changes (e.g. conditional remount), re-pin the new ScrollView
705
+ // When the ScrollView changes (e.g. conditional remount), re-detect the new ScrollView
630
706
  // and request layout so BottomSheetBehavior re-discovers the nested scrolling child.
631
707
  override fun containerViewScrollViewDidChange() {
632
708
  setupScrollable()
@@ -643,6 +719,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
643
719
  viewController.positionFooter()
644
720
  }
645
721
 
722
+ override fun containerViewFooterDidLayout() {
723
+ viewController.positionFooter()
724
+ }
725
+
646
726
  override fun containerViewPeekDidChangeSize(width: Int, height: Int) {
647
727
  // Peek content height affects peek detents
648
728
  updateSheetIfNeeded()
@@ -672,16 +752,5 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
672
752
  * of whichever window this view is in - whether that's the activity's window or a
673
753
  * Modal's dialog window.
674
754
  */
675
- override fun findRootContainerView(): ViewGroup? {
676
- var current: android.view.ViewParent? = parent
677
-
678
- while (current != null) {
679
- if (current is ViewGroup && current.id == android.R.id.content) {
680
- return current
681
- }
682
- current = current.parent
683
- }
684
-
685
- return reactContext.currentActivity?.findViewById(android.R.id.content)
686
- }
755
+ override fun findRootContainerView(): ViewGroup? = findRootContainerView(reactContext)
687
756
  }