@lodev09/react-native-true-sheet 2.0.6 → 4.0.0-beta.0

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 (194) hide show
  1. package/README.md +36 -8
  2. package/RNTrueSheet.podspec +20 -0
  3. package/android/build.gradle +26 -14
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +108 -0
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerViewManager.kt +21 -0
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +46 -0
  7. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +21 -0
  8. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +47 -0
  9. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterViewManager.kt +21 -0
  10. package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +165 -0
  11. package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +36 -4
  12. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +257 -303
  13. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +855 -0
  14. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +104 -82
  15. package/android/src/main/java/com/lodev09/truesheet/events/DetentChangeEvent.kt +26 -0
  16. package/android/src/main/java/com/lodev09/truesheet/events/DidDismissEvent.kt +20 -0
  17. package/android/src/main/java/com/lodev09/truesheet/events/DidPresentEvent.kt +26 -0
  18. package/android/src/main/java/com/lodev09/truesheet/events/DragBeginEvent.kt +26 -0
  19. package/android/src/main/java/com/lodev09/truesheet/events/DragChangeEvent.kt +26 -0
  20. package/android/src/main/java/com/lodev09/truesheet/events/DragEndEvent.kt +26 -0
  21. package/android/src/main/java/com/lodev09/truesheet/events/MountEvent.kt +20 -0
  22. package/android/src/main/java/com/lodev09/truesheet/events/PositionChangeEvent.kt +32 -0
  23. package/android/src/main/java/com/lodev09/truesheet/events/SizeChangeEvent.kt +27 -0
  24. package/android/src/main/java/com/lodev09/truesheet/events/WillDismissEvent.kt +20 -0
  25. package/android/src/main/java/com/lodev09/truesheet/events/WillPresentEvent.kt +26 -0
  26. package/android/src/main/java/com/lodev09/truesheet/{core/Utils.kt → utils/ScreenUtils.kt} +47 -17
  27. package/android/src/main/res/values/styles.xml +8 -0
  28. package/ios/TrueSheetComponentDescriptor.h +24 -0
  29. package/ios/TrueSheetContainerView.h +47 -0
  30. package/ios/TrueSheetContainerView.mm +117 -0
  31. package/ios/TrueSheetContentView.h +37 -0
  32. package/ios/TrueSheetContentView.mm +114 -0
  33. package/ios/TrueSheetFooterView.h +27 -0
  34. package/ios/TrueSheetFooterView.mm +101 -0
  35. package/ios/TrueSheetModule.h +44 -0
  36. package/ios/TrueSheetModule.mm +133 -0
  37. package/ios/TrueSheetView.h +53 -0
  38. package/ios/TrueSheetView.mm +433 -0
  39. package/ios/TrueSheetViewController.h +53 -0
  40. package/ios/TrueSheetViewController.mm +649 -0
  41. package/ios/events/OnDetentChangeEvent.h +28 -0
  42. package/ios/events/OnDetentChangeEvent.mm +30 -0
  43. package/ios/events/OnDidDismissEvent.h +26 -0
  44. package/ios/events/OnDidDismissEvent.mm +25 -0
  45. package/ios/events/OnDidPresentEvent.h +28 -0
  46. package/ios/events/OnDidPresentEvent.mm +30 -0
  47. package/ios/events/OnDragBeginEvent.h +28 -0
  48. package/ios/events/OnDragBeginEvent.mm +30 -0
  49. package/ios/events/OnDragChangeEvent.h +28 -0
  50. package/ios/events/OnDragChangeEvent.mm +30 -0
  51. package/ios/events/OnDragEndEvent.h +28 -0
  52. package/ios/events/OnDragEndEvent.mm +30 -0
  53. package/ios/events/OnMountEvent.h +26 -0
  54. package/ios/events/OnMountEvent.mm +25 -0
  55. package/ios/events/OnPositionChangeEvent.h +29 -0
  56. package/ios/events/OnPositionChangeEvent.mm +32 -0
  57. package/ios/events/OnSizeChangeEvent.h +28 -0
  58. package/ios/events/OnSizeChangeEvent.mm +30 -0
  59. package/ios/events/OnWillDismissEvent.h +26 -0
  60. package/ios/events/OnWillDismissEvent.mm +25 -0
  61. package/ios/events/OnWillPresentEvent.h +28 -0
  62. package/ios/events/OnWillPresentEvent.mm +30 -0
  63. package/ios/utils/GestureUtil.h +25 -0
  64. package/ios/utils/GestureUtil.mm +26 -0
  65. package/ios/utils/LayoutUtil.h +44 -0
  66. package/ios/utils/LayoutUtil.mm +50 -0
  67. package/ios/utils/WindowUtil.h +27 -0
  68. package/ios/utils/WindowUtil.mm +42 -0
  69. package/lib/module/TrueSheet.js +231 -135
  70. package/lib/module/TrueSheet.js.map +1 -1
  71. package/lib/module/TrueSheetGrabber.js +16 -14
  72. package/lib/module/TrueSheetGrabber.js.map +1 -1
  73. package/lib/module/fabric/TrueSheetContainerViewNativeComponent.ts +8 -0
  74. package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +8 -0
  75. package/lib/module/fabric/TrueSheetFooterViewNativeComponent.ts +8 -0
  76. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +63 -0
  77. package/lib/module/index.js +1 -0
  78. package/lib/module/index.js.map +1 -1
  79. package/lib/module/reanimated/ReanimatedTrueSheet.js +87 -0
  80. package/lib/module/reanimated/ReanimatedTrueSheet.js.map +1 -0
  81. package/lib/module/reanimated/ReanimatedTrueSheetProvider.js +72 -0
  82. package/lib/module/reanimated/ReanimatedTrueSheetProvider.js.map +1 -0
  83. package/lib/module/reanimated/index.js +6 -0
  84. package/lib/module/reanimated/index.js.map +1 -0
  85. package/lib/module/reanimated/useReanimatedPositionChangeHandler.js +19 -0
  86. package/lib/module/reanimated/useReanimatedPositionChangeHandler.js.map +1 -0
  87. package/lib/module/specs/NativeTrueSheetModule.js +12 -0
  88. package/lib/module/specs/NativeTrueSheetModule.js.map +1 -0
  89. package/lib/typescript/package.json +1 -0
  90. package/lib/typescript/src/TrueSheet.d.ts +79 -0
  91. package/lib/typescript/src/TrueSheet.d.ts.map +1 -0
  92. package/lib/typescript/src/TrueSheet.types.d.ts +260 -0
  93. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -0
  94. package/lib/typescript/{commonjs/src → src}/TrueSheetGrabber.d.ts +1 -1
  95. package/lib/typescript/src/TrueSheetGrabber.d.ts.map +1 -0
  96. package/lib/typescript/src/fabric/TrueSheetContainerViewNativeComponent.d.ts +6 -0
  97. package/lib/typescript/src/fabric/TrueSheetContainerViewNativeComponent.d.ts.map +1 -0
  98. package/lib/typescript/src/fabric/TrueSheetContentViewNativeComponent.d.ts +6 -0
  99. package/lib/typescript/src/fabric/TrueSheetContentViewNativeComponent.d.ts.map +1 -0
  100. package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts +6 -0
  101. package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts.map +1 -0
  102. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +44 -0
  103. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -0
  104. package/lib/typescript/{commonjs/src → src}/index.d.ts +1 -0
  105. package/lib/typescript/src/index.d.ts.map +1 -0
  106. package/lib/typescript/src/reanimated/ReanimatedTrueSheet.d.ts +43 -0
  107. package/lib/typescript/src/reanimated/ReanimatedTrueSheet.d.ts.map +1 -0
  108. package/lib/typescript/src/reanimated/ReanimatedTrueSheetProvider.d.ts +57 -0
  109. package/lib/typescript/src/reanimated/ReanimatedTrueSheetProvider.d.ts.map +1 -0
  110. package/lib/typescript/src/reanimated/index.d.ts +4 -0
  111. package/lib/typescript/src/reanimated/index.d.ts.map +1 -0
  112. package/lib/typescript/src/reanimated/useReanimatedPositionChangeHandler.d.ts +6 -0
  113. package/lib/typescript/src/reanimated/useReanimatedPositionChangeHandler.d.ts.map +1 -0
  114. package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts +34 -0
  115. package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts.map +1 -0
  116. package/package.json +104 -75
  117. package/react-native.config.js +17 -0
  118. package/src/TrueSheet.tsx +285 -188
  119. package/src/TrueSheet.types.ts +119 -106
  120. package/src/TrueSheetGrabber.tsx +29 -28
  121. package/src/__mocks__/index.js +60 -12
  122. package/src/fabric/TrueSheetContainerViewNativeComponent.ts +8 -0
  123. package/src/fabric/TrueSheetContentViewNativeComponent.ts +8 -0
  124. package/src/fabric/TrueSheetFooterViewNativeComponent.ts +8 -0
  125. package/src/fabric/TrueSheetViewNativeComponent.ts +63 -0
  126. package/src/index.ts +4 -3
  127. package/src/reanimated/ReanimatedTrueSheet.tsx +95 -0
  128. package/src/reanimated/ReanimatedTrueSheetProvider.tsx +92 -0
  129. package/src/reanimated/index.ts +3 -0
  130. package/src/reanimated/useReanimatedPositionChangeHandler.ts +26 -0
  131. package/src/specs/NativeTrueSheetModule.ts +38 -0
  132. package/TrueSheet.podspec +0 -49
  133. package/android/src/main/java/com/lodev09/truesheet/TrueSheetDialog.kt +0 -413
  134. package/android/src/main/java/com/lodev09/truesheet/TrueSheetEvent.kt +0 -22
  135. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewModule.kt +0 -63
  136. package/android/src/main/java/com/lodev09/truesheet/core/KeyboardManager.kt +0 -58
  137. package/android/src/main/java/com/lodev09/truesheet/core/RootSheetView.kt +0 -102
  138. package/ios/Extensions/UIBlurEffect+withTint.swift +0 -62
  139. package/ios/Extensions/UIView+pinTo.swift +0 -74
  140. package/ios/Extensions/UIViewController+detentForSize.swift +0 -134
  141. package/ios/TrueSheet-Bridging-Header.h +0 -14
  142. package/ios/TrueSheetEvent.swift +0 -48
  143. package/ios/TrueSheetView.swift +0 -467
  144. package/ios/TrueSheetViewController.swift +0 -275
  145. package/ios/TrueSheetViewManager.m +0 -53
  146. package/ios/TrueSheetViewManager.swift +0 -48
  147. package/ios/Utils/Logger.swift +0 -39
  148. package/ios/Utils/Promise.swift +0 -25
  149. package/lib/commonjs/TrueSheet.js +0 -258
  150. package/lib/commonjs/TrueSheet.js.map +0 -1
  151. package/lib/commonjs/TrueSheet.types.js +0 -6
  152. package/lib/commonjs/TrueSheet.types.js.map +0 -1
  153. package/lib/commonjs/TrueSheetFooter.js +0 -19
  154. package/lib/commonjs/TrueSheetFooter.js.map +0 -1
  155. package/lib/commonjs/TrueSheetGrabber.js +0 -54
  156. package/lib/commonjs/TrueSheetGrabber.js.map +0 -1
  157. package/lib/commonjs/TrueSheetModule.js +0 -19
  158. package/lib/commonjs/TrueSheetModule.js.map +0 -1
  159. package/lib/commonjs/__mocks__/index.js +0 -52
  160. package/lib/commonjs/__mocks__/index.js.map +0 -1
  161. package/lib/commonjs/index.js +0 -39
  162. package/lib/commonjs/index.js.map +0 -1
  163. package/lib/module/TrueSheetFooter.js +0 -14
  164. package/lib/module/TrueSheetFooter.js.map +0 -1
  165. package/lib/module/TrueSheetModule.js +0 -15
  166. package/lib/module/TrueSheetModule.js.map +0 -1
  167. package/lib/module/__mocks__/index.js +0 -21
  168. package/lib/module/__mocks__/index.js.map +0 -1
  169. package/lib/typescript/commonjs/package.json +0 -1
  170. package/lib/typescript/commonjs/src/TrueSheet.d.ts +0 -70
  171. package/lib/typescript/commonjs/src/TrueSheet.d.ts.map +0 -1
  172. package/lib/typescript/commonjs/src/TrueSheet.types.d.ts +0 -241
  173. package/lib/typescript/commonjs/src/TrueSheet.types.d.ts.map +0 -1
  174. package/lib/typescript/commonjs/src/TrueSheetFooter.d.ts +0 -7
  175. package/lib/typescript/commonjs/src/TrueSheetFooter.d.ts.map +0 -1
  176. package/lib/typescript/commonjs/src/TrueSheetGrabber.d.ts.map +0 -1
  177. package/lib/typescript/commonjs/src/TrueSheetModule.d.ts +0 -2
  178. package/lib/typescript/commonjs/src/TrueSheetModule.d.ts.map +0 -1
  179. package/lib/typescript/commonjs/src/index.d.ts.map +0 -1
  180. package/lib/typescript/module/src/TrueSheet.d.ts +0 -70
  181. package/lib/typescript/module/src/TrueSheet.d.ts.map +0 -1
  182. package/lib/typescript/module/src/TrueSheet.types.d.ts +0 -241
  183. package/lib/typescript/module/src/TrueSheet.types.d.ts.map +0 -1
  184. package/lib/typescript/module/src/TrueSheetFooter.d.ts +0 -7
  185. package/lib/typescript/module/src/TrueSheetFooter.d.ts.map +0 -1
  186. package/lib/typescript/module/src/TrueSheetGrabber.d.ts +0 -39
  187. package/lib/typescript/module/src/TrueSheetGrabber.d.ts.map +0 -1
  188. package/lib/typescript/module/src/TrueSheetModule.d.ts +0 -2
  189. package/lib/typescript/module/src/TrueSheetModule.d.ts.map +0 -1
  190. package/lib/typescript/module/src/index.d.ts +0 -4
  191. package/lib/typescript/module/src/index.d.ts.map +0 -1
  192. package/src/TrueSheetFooter.tsx +0 -17
  193. package/src/TrueSheetModule.ts +0 -19
  194. /package/lib/{typescript/module → module}/package.json +0 -0
@@ -0,0 +1,855 @@
1
+ package com.lodev09.truesheet
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.graphics.Color
5
+ import android.graphics.drawable.ShapeDrawable
6
+ import android.graphics.drawable.shapes.RoundRectShape
7
+ import android.view.MotionEvent
8
+ import android.view.View
9
+ import android.view.WindowManager
10
+ import android.view.accessibility.AccessibilityNodeInfo
11
+ import android.widget.FrameLayout
12
+ import androidx.core.view.isNotEmpty
13
+ import com.facebook.react.R
14
+ import com.facebook.react.common.annotations.UnstableReactNativeAPI
15
+ import com.facebook.react.uimanager.JSPointerDispatcher
16
+ import com.facebook.react.uimanager.JSTouchDispatcher
17
+ import com.facebook.react.uimanager.PixelUtil.dpToPx
18
+ import com.facebook.react.uimanager.PixelUtil.pxToDp
19
+ import com.facebook.react.uimanager.RootView
20
+ import com.facebook.react.uimanager.ThemedReactContext
21
+ import com.facebook.react.uimanager.events.EventDispatcher
22
+ import com.facebook.react.views.view.ReactViewGroup
23
+ import com.google.android.material.bottomsheet.BottomSheetBehavior
24
+ import com.google.android.material.bottomsheet.BottomSheetDialog
25
+ import com.lodev09.truesheet.utils.ScreenUtils
26
+
27
+ data class DetentInfo(val index: Int, val position: Float)
28
+
29
+ /**
30
+ * Delegate protocol for TrueSheetViewController lifecycle and interaction events.
31
+ * Similar to iOS TrueSheetViewControllerDelegate pattern.
32
+ */
33
+ interface TrueSheetViewControllerDelegate {
34
+ fun viewControllerWillPresent(index: Int, position: Float)
35
+ fun viewControllerDidPresent(index: Int, position: Float)
36
+ fun viewControllerWillDismiss()
37
+ fun viewControllerDidDismiss()
38
+ fun viewControllerDidChangeDetent(index: Int, position: Float)
39
+ fun viewControllerDidDragBegin(index: Int, position: Float)
40
+ fun viewControllerDidDragChange(index: Int, position: Float)
41
+ fun viewControllerDidDragEnd(index: Int, position: Float)
42
+ fun viewControllerDidChangePosition(index: Int, position: Float, transitioning: Boolean)
43
+ fun viewControllerDidChangeSize(width: Int, height: Int)
44
+ }
45
+
46
+ /**
47
+ * TrueSheetViewController manages the bottom sheet dialog lifecycle and properties.
48
+ * Similar to iOS TrueSheetViewController pattern.
49
+ *
50
+ * This view acts as both the RootView (handles touch events) and the controller (manages dialog).
51
+ */
52
+ @SuppressLint("ClickableViewAccessibility", "ViewConstructor")
53
+ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
54
+ ReactViewGroup(reactContext),
55
+ RootView {
56
+
57
+ companion object {
58
+ private const val TAG_NAME = "TrueSheet"
59
+ }
60
+
61
+ // ==================== RootView Touch Handling ====================
62
+
63
+ internal var eventDispatcher: EventDispatcher? = null
64
+
65
+ private val jSTouchDispatcher = JSTouchDispatcher(this)
66
+ private var jSPointerDispatcher: JSPointerDispatcher? = null
67
+
68
+ /**
69
+ * Delegate for handling view controller events
70
+ */
71
+ var delegate: TrueSheetViewControllerDelegate? = null
72
+
73
+ /**
74
+ * The BottomSheetDialog instance - created lazily when container mounts
75
+ */
76
+ private var dialog: BottomSheetDialog? = null
77
+
78
+ /**
79
+ * The sheet behavior from the dialog
80
+ */
81
+ private val behavior: BottomSheetBehavior<FrameLayout>?
82
+ get() = dialog?.behavior
83
+
84
+ /**
85
+ * The sheet container view from Material BottomSheetDialog (our parent)
86
+ */
87
+ private val sheetContainer: FrameLayout?
88
+ get() = this.parent as? FrameLayout
89
+
90
+ /**
91
+ * The actual bottom sheet view used by Material BottomSheetBehavior
92
+ * This is the view whose position changes during drag
93
+ */
94
+ private val bottomSheetView: FrameLayout?
95
+ get() = dialog?.findViewById(com.google.android.material.R.id.design_bottom_sheet)
96
+
97
+ /**
98
+ * Our sheet container view from this root view's only child
99
+ */
100
+ private val containerView: TrueSheetContainerView?
101
+ get() = if (this.isNotEmpty()) {
102
+ this.getChildAt(0) as? TrueSheetContainerView
103
+ } else {
104
+ null
105
+ }
106
+
107
+ /**
108
+ * Footer view from the container
109
+ */
110
+ private val footerView: TrueSheetFooterView?
111
+ get() = containerView?.footerView
112
+
113
+ /**
114
+ * Track if the dialog is currently being dragged
115
+ */
116
+ private var isDragging = false
117
+
118
+ /**
119
+ * Track if the sheet has been presented (after onShow callback)
120
+ */
121
+ var isPresented = false
122
+ private set
123
+
124
+ private val edgeToEdgeEnabled: Boolean
125
+ get() {
126
+ // Auto-enable edge-to-edge for Android 16+ (API level 36) if not explicitly set
127
+ val defaultEnabled = android.os.Build.VERSION.SDK_INT >= 36
128
+ return BuildConfig.EDGE_TO_EDGE_ENABLED || dialog?.edgeToEdgeEnabled == true || defaultEnabled
129
+ }
130
+
131
+ /**
132
+ * Whether to allow the sheet to extend behind the status bar in edge-to-edge mode
133
+ */
134
+ var edgeToEdgeFullScreen: Boolean = false
135
+
136
+ /**
137
+ * Top inset to apply to sheet max height calculation (only when not edgeToEdgeFullScreen)
138
+ */
139
+ private val sheetTopInset: Int
140
+ get() = if (edgeToEdgeEnabled && !edgeToEdgeFullScreen) ScreenUtils.getStatusBarHeight(reactContext) else 0
141
+
142
+ /**
143
+ * Current active detent index
144
+ */
145
+ var currentDetentIndex: Int = -1
146
+ private set
147
+
148
+ /**
149
+ * Promise callback to be invoked after present is called
150
+ */
151
+ var presentPromise: (() -> Unit)? = null
152
+
153
+ /**
154
+ * Promise callback to be invoked after dismiss is called
155
+ */
156
+ var dismissPromise: (() -> Unit)? = null
157
+
158
+ // ==================== Properties ====================
159
+
160
+ /**
161
+ * Specify whether the sheet background is dimmed.
162
+ * Set to `false` to allow interaction with the background components.
163
+ */
164
+ var dimmed = true
165
+
166
+ /**
167
+ * The detent index that the sheet should start to dim the background.
168
+ * This is ignored if `dimmed` is set to `false`.
169
+ */
170
+ var dimmedIndex = 0
171
+
172
+ /**
173
+ * The maximum window height
174
+ */
175
+ var maxScreenHeight = 0
176
+
177
+ var maxSheetHeight: Int? = null
178
+
179
+ /**
180
+ * The content height from the container view.
181
+ * Set by the host view when content size changes.
182
+ */
183
+ var contentHeight: Int = 0
184
+
185
+ var dismissible: Boolean = true
186
+ set(value) {
187
+ field = value
188
+ dialog?.apply {
189
+ setCanceledOnTouchOutside(value)
190
+ setCancelable(value)
191
+ behavior.isHideable = value
192
+ }
193
+ }
194
+
195
+ var cornerRadius: Float = 0f
196
+ var sheetBackgroundColor: Int = Color.WHITE
197
+ var detents = mutableListOf(0.5, 1.0)
198
+
199
+ private var windowAnimation: Int = 0
200
+
201
+ init {
202
+ maxScreenHeight = ScreenUtils.getScreenHeight(reactContext, edgeToEdgeEnabled)
203
+ jSPointerDispatcher = JSPointerDispatcher(this)
204
+ }
205
+
206
+ // ==================== Lifecycle ====================
207
+
208
+ /**
209
+ * Creates the dialog instance. Should be called when container view is mounted.
210
+ */
211
+ fun createDialog() {
212
+ if (dialog != null) return
213
+
214
+ val style = if (edgeToEdgeEnabled) {
215
+ com.lodev09.truesheet.R.style.TrueSheetEdgeToEdgeEnabledDialog
216
+ } else {
217
+ 0
218
+ }
219
+
220
+ dialog = BottomSheetDialog(reactContext, style).apply {
221
+ setContentView(this@TrueSheetViewController)
222
+
223
+ // Setup window params
224
+ window?.apply {
225
+ // Store current windowAnimation value to toggle later
226
+ windowAnimation = attributes.windowAnimations
227
+ }
228
+
229
+ // Setup dialog lifecycle listeners
230
+ setupDialogListeners(this)
231
+
232
+ // Setup bottom sheet behavior callbacks
233
+ setupBottomSheetBehavior(this)
234
+
235
+ // Apply initial properties
236
+ setCanceledOnTouchOutside(dismissible)
237
+ setCancelable(dismissible)
238
+ behavior.isHideable = dismissible
239
+
240
+ // Apply background color and corner radius
241
+ setupBackground()
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Cleans up the dialog instance. Called when dismissed to ensure clean state.
247
+ */
248
+ private fun cleanupDialog() {
249
+ dialog?.apply {
250
+ setOnShowListener(null)
251
+ setOnCancelListener(null)
252
+ setOnDismissListener(null)
253
+ }
254
+
255
+ // Remove this view from its parent to allow re-attachment on next presentation
256
+ sheetContainer?.removeView(this)
257
+
258
+ dialog = null
259
+ isDragging = false
260
+ isPresented = false
261
+ }
262
+
263
+ /**
264
+ * Setup dialog lifecycle listeners
265
+ */
266
+ private fun setupDialogListeners(dialog: BottomSheetDialog) {
267
+ // Setup listener when the dialog has been presented
268
+ dialog.setOnShowListener {
269
+ isPresented = true
270
+
271
+ // Re-enable animation
272
+ resetAnimation()
273
+
274
+ // Wait for the sheet to settle before notifying didPresent
275
+ // The sheet animates to its final position after onShow fires
276
+ sheetContainer?.post {
277
+ // Notify delegate with the settled position
278
+ val detentInfo = getDetentInfoForIndex(currentDetentIndex)
279
+ delegate?.viewControllerDidPresent(detentInfo.index, detentInfo.position)
280
+
281
+ // Emit position change with transitioning=true so Reanimated can animate it
282
+ delegate?.viewControllerDidChangePosition(detentInfo.index, detentInfo.position, transitioning = true)
283
+
284
+ // Resolve the present promise
285
+ presentPromise?.let { promise ->
286
+ promise()
287
+ presentPromise = null
288
+ }
289
+
290
+ // Initialize footer position after layout is complete
291
+ positionFooter()
292
+ }
293
+ }
294
+
295
+ // Setup listener when the dialog is about to be dismissed
296
+ dialog.setOnCancelListener {
297
+ // Notify delegate
298
+ delegate?.viewControllerWillDismiss()
299
+ }
300
+
301
+ // Setup listener when the dialog has been dismissed
302
+ dialog.setOnDismissListener {
303
+ // Resolve the dismiss promise
304
+ dismissPromise?.let { promise ->
305
+ promise()
306
+ dismissPromise = null
307
+ }
308
+
309
+ // Notify delegate
310
+ delegate?.viewControllerDidDismiss()
311
+
312
+ // Clean up the dialog for next presentation
313
+ cleanupDialog()
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Setup bottom sheet behavior callbacks
319
+ */
320
+ private fun setupBottomSheetBehavior(dialog: BottomSheetDialog) {
321
+ dialog.behavior.addBottomSheetCallback(
322
+ object : BottomSheetBehavior.BottomSheetCallback() {
323
+ override fun onSlide(sheetView: View, slideOffset: Float) {
324
+ val behavior = behavior ?: return
325
+
326
+ // Emit position change event continuously during slide
327
+ // Set transitioning=false during drag to get real-time position updates
328
+ val detentInfo = getCurrentDetentInfo(sheetView)
329
+ delegate?.viewControllerDidChangePosition(detentInfo.index, detentInfo.position, transitioning = false)
330
+
331
+ when (behavior.state) {
332
+ // For consistency with iOS, we consider SETTLING as dragging change
333
+ BottomSheetBehavior.STATE_DRAGGING,
334
+ BottomSheetBehavior.STATE_SETTLING -> handleDragChange(sheetView)
335
+
336
+ else -> { }
337
+ }
338
+
339
+ // Update footer position during slide
340
+ positionFooter(slideOffset)
341
+ }
342
+
343
+ override fun onStateChanged(sheetView: View, newState: Int) {
344
+ // Handle STATE_HIDDEN before checking isPresented
345
+ // This ensures we can dismiss even if dialog state gets out of sync
346
+ if (newState == BottomSheetBehavior.STATE_HIDDEN) {
347
+ dismiss()
348
+ return
349
+ }
350
+
351
+ if (!isPresented) return
352
+
353
+ when (newState) {
354
+ // When changed to dragging, we know that the drag has started
355
+ BottomSheetBehavior.STATE_DRAGGING -> handleDragBegin(sheetView)
356
+
357
+ // Either of the following state determines drag end
358
+ BottomSheetBehavior.STATE_EXPANDED,
359
+ BottomSheetBehavior.STATE_COLLAPSED,
360
+ BottomSheetBehavior.STATE_HALF_EXPANDED -> handleDragEnd(newState)
361
+
362
+ else -> {}
363
+ }
364
+ }
365
+ }
366
+ )
367
+ }
368
+
369
+ // ==================== Presentation ====================
370
+
371
+ /**
372
+ * Present the sheet.
373
+ */
374
+ fun present(detentIndex: Int, animated: Boolean = true) {
375
+ val dialog = this.dialog ?: run {
376
+ // Dialog not created yet - this shouldn't happen but handle gracefully
377
+ return
378
+ }
379
+
380
+ currentDetentIndex = detentIndex
381
+ setupDimmedBackground(detentIndex)
382
+
383
+ if (isPresented) {
384
+ // For consistency with iOS, we notify detent change immediately
385
+ // when already presented (not waiting for state to change)
386
+ val detentInfo = getDetentInfoForIndex(detentIndex)
387
+ delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position)
388
+
389
+ // Note: onSlide will be called during resize animation, no need to emit position change here
390
+ setStateForDetentIndex(detentIndex)
391
+ } else {
392
+ // Reset drag state before presenting
393
+ isDragging = false
394
+
395
+ setupSheetDetents()
396
+ setStateForDetentIndex(detentIndex)
397
+
398
+ // Notify delegate before showing
399
+ val detentInfo = getDetentInfoForIndex(detentIndex)
400
+ delegate?.viewControllerWillPresent(detentInfo.index, detentInfo.position)
401
+
402
+ if (!animated) {
403
+ // Disable animation
404
+ dialog.window?.setWindowAnimations(0)
405
+ }
406
+
407
+ dialog.show()
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Dismiss the sheet.
413
+ */
414
+ fun dismiss() {
415
+ this.post {
416
+ // Emit position change with transitioning=true to animate dismissal
417
+ // Use maxScreenHeight as the off-screen position (sheet slides down off screen)
418
+ val offScreenPosition = maxScreenHeight.pxToDp()
419
+ delegate?.viewControllerDidChangePosition(currentDetentIndex, offScreenPosition, transitioning = true)
420
+ }
421
+
422
+ dialog?.dismiss()
423
+ }
424
+
425
+ // ==================== Configuration ====================
426
+
427
+ /**
428
+ * Setup sheet detents based on the detent preference.
429
+ */
430
+ fun setupSheetDetents() {
431
+ val behavior = this.behavior ?: return
432
+
433
+ // Configure sheet sizes
434
+ behavior.apply {
435
+ skipCollapsed = false
436
+ isFitToContents = true
437
+
438
+ // m3 max width 640dp
439
+ maxWidth = 640.0.dpToPx().toInt()
440
+
441
+ when (detents.size) {
442
+ 1 -> {
443
+ val detentHeight = getDetentHeight(detents[0])
444
+ maxHeight = detentHeight
445
+ skipCollapsed = true
446
+ }
447
+
448
+ 2 -> {
449
+ val peekHeight = getDetentHeight(detents[0])
450
+ val maxHeightValue = getDetentHeight(detents[1])
451
+ maxHeight = maxHeightValue
452
+ setPeekHeight(peekHeight, isPresented)
453
+ }
454
+
455
+ 3 -> {
456
+ // Enables half expanded
457
+ isFitToContents = false
458
+
459
+ val peekHeightValue = getDetentHeight(detents[0])
460
+ val middleDetentHeight = getDetentHeight(detents[1])
461
+ val maxHeightValue = getDetentHeight(detents[2])
462
+
463
+ setPeekHeight(peekHeightValue, isPresented)
464
+ maxHeight = maxHeightValue
465
+ expandedOffset = sheetTopInset
466
+ halfExpandedRatio = minOf(middleDetentHeight.toFloat() / maxScreenHeight.toFloat(), 1.0f)
467
+ }
468
+ }
469
+
470
+ // Force a layout update when sheet is presented (e.g., during rotation)
471
+ // This ensures the container respects the new maxHeight constraint
472
+ if (isPresented) {
473
+ sheetContainer?.apply {
474
+ val params = layoutParams
475
+ params.height = maxHeight
476
+ layoutParams = params
477
+ }
478
+ }
479
+ }
480
+ }
481
+
482
+ /**
483
+ * Setup background color and corner radius.
484
+ */
485
+ fun setupBackground() {
486
+ sheetContainer?.apply {
487
+ val outerRadii = floatArrayOf(
488
+ cornerRadius,
489
+ cornerRadius,
490
+ cornerRadius,
491
+ cornerRadius,
492
+ 0f,
493
+ 0f,
494
+ 0f,
495
+ 0f
496
+ )
497
+
498
+ val background = ShapeDrawable(RoundRectShape(outerRadii, null, null))
499
+ background.paint.color = sheetBackgroundColor
500
+
501
+ this.background = background
502
+ this.clipToOutline = true
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Setup dimmed sheet.
508
+ * `dimmedIndex` will further customize the dimming behavior.
509
+ */
510
+ fun setupDimmedBackground(detentIndex: Int) {
511
+ val dialog = this.dialog ?: return
512
+ dialog.window?.apply {
513
+ val view = findViewById<View>(com.google.android.material.R.id.touch_outside)
514
+
515
+ if (dimmed && detentIndex >= dimmedIndex) {
516
+ // Remove touch listener
517
+ view.setOnTouchListener(null)
518
+
519
+ // Add the dimmed background
520
+ setFlags(
521
+ WindowManager.LayoutParams.FLAG_DIM_BEHIND,
522
+ WindowManager.LayoutParams.FLAG_DIM_BEHIND
523
+ )
524
+
525
+ dialog.setCanceledOnTouchOutside(dismissible)
526
+ } else {
527
+ // Override the background touch and pass it to the components outside
528
+ view.setOnTouchListener { v, event ->
529
+ event.setLocation(event.rawX - v.x, event.rawY - v.y)
530
+ reactContext.currentActivity?.dispatchTouchEvent(event)
531
+ false
532
+ }
533
+
534
+ // Remove the dimmed background
535
+ clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
536
+
537
+ dialog.setCanceledOnTouchOutside(false)
538
+ }
539
+ }
540
+ }
541
+
542
+ fun resetAnimation() {
543
+ dialog?.window?.apply {
544
+ setWindowAnimations(windowAnimation)
545
+ }
546
+ }
547
+
548
+ fun positionFooter(slideOffset: Float? = null) {
549
+ val footer = footerView ?: return
550
+ val bottomSheet = bottomSheetView ?: return
551
+ val footerHeight = footer.height
552
+
553
+ val bottomSheetY = ScreenUtils.getScreenY(bottomSheet)
554
+
555
+ // Calculate footer Y position based on bottom sheet position
556
+ var footerY = (maxScreenHeight - bottomSheetY - footerHeight).toFloat()
557
+
558
+ // Animate footer down with sheet when below peek height
559
+ if (slideOffset != null && slideOffset < 0) {
560
+ footerY -= (footerHeight * slideOffset)
561
+ }
562
+
563
+ // Clamp footer position to prevent it from going off screen when positioning at the top
564
+ // This happens when fullScreen is enabled in edge-to-edge mode
565
+ val statusBarHeight = ScreenUtils.getStatusBarHeight(reactContext)
566
+ val maxAllowedY = (maxScreenHeight - statusBarHeight - footerHeight).toFloat()
567
+ footer.y = minOf(footerY, maxAllowedY)
568
+ }
569
+
570
+ /**
571
+ * Set the state based for the given detent index.
572
+ */
573
+ fun setStateForDetentIndex(index: Int) {
574
+ behavior?.state = getStateForDetentIndex(index)
575
+ }
576
+
577
+ fun setSoftInputMode(mode: Int) {
578
+ dialog?.window?.setSoftInputMode(mode)
579
+ }
580
+
581
+ // ==================== Drag Handling ====================
582
+
583
+ /**
584
+ * Get current detent info from sheet view position
585
+ */
586
+ private fun getCurrentDetentInfo(sheetView: View): DetentInfo {
587
+ // Get the Y position in screen coordinates (like iOS presentedView.frame.origin.y)
588
+ val screenY = ScreenUtils.getScreenY(sheetView)
589
+ val position = screenY.pxToDp()
590
+ return DetentInfo(currentDetentIndex, position)
591
+ }
592
+
593
+ /**
594
+ * Handle drag begin
595
+ */
596
+ private fun handleDragBegin(sheetView: View) {
597
+ val detentInfo = getCurrentDetentInfo(sheetView)
598
+ delegate?.viewControllerDidDragBegin(detentInfo.index, detentInfo.position)
599
+ isDragging = true
600
+ }
601
+
602
+ /**
603
+ * Handle drag change
604
+ */
605
+ private fun handleDragChange(sheetView: View) {
606
+ if (!isDragging) return
607
+
608
+ val detentInfo = getCurrentDetentInfo(sheetView)
609
+ delegate?.viewControllerDidDragChange(detentInfo.index, detentInfo.position)
610
+ }
611
+
612
+ /**
613
+ * Handle drag end
614
+ */
615
+ private fun handleDragEnd(state: Int) {
616
+ if (!isDragging) return
617
+
618
+ // For consistency with iOS,
619
+ // we only handle state changes after dragging.
620
+ //
621
+ // Changing detent programmatically is handled via the present method.
622
+ val detentInfo = getDetentInfoForState(state)
623
+ detentInfo?.let {
624
+ // Notify delegate of drag end
625
+ delegate?.viewControllerDidDragEnd(it.index, it.position)
626
+
627
+ if (it.index != currentDetentIndex) {
628
+ presentPromise?.let { promise ->
629
+ promise()
630
+ presentPromise = null
631
+ }
632
+
633
+ currentDetentIndex = it.index
634
+ setupDimmedBackground(it.index)
635
+
636
+ // Notify delegate of detent change
637
+ delegate?.viewControllerDidChangeDetent(it.index, it.position)
638
+ }
639
+ }
640
+
641
+ isDragging = false
642
+ }
643
+
644
+ // ==================== Detent Calculations ====================
645
+
646
+ /**
647
+ * Get the height value based on the detent config value.
648
+ */
649
+ private fun getDetentHeight(detent: Double): Int {
650
+ val height: Int = if (detent == -1.0) {
651
+ // -1.0 represents "auto"
652
+ contentHeight
653
+ } else {
654
+ if (detent <= 0.0 || detent > 1.0) {
655
+ throw IllegalArgumentException("TrueSheet: detent fraction ($detent) must be between 0 and 1")
656
+ }
657
+ (detent * maxScreenHeight).toInt()
658
+ }
659
+
660
+ // Apply top inset when edge-to-edge is enabled and fullScreen is false
661
+ val maxAllowedHeight = maxScreenHeight - sheetTopInset
662
+ val finalHeight = maxSheetHeight?.let { minOf(height, it, maxAllowedHeight) } ?: minOf(height, maxAllowedHeight)
663
+ return finalHeight
664
+ }
665
+
666
+ /**
667
+ * Determines the state based from the given detent index.
668
+ */
669
+ private fun getStateForDetentIndex(index: Int): Int =
670
+ when (detents.size) {
671
+ 1 -> {
672
+ BottomSheetBehavior.STATE_EXPANDED
673
+ }
674
+
675
+ 2 -> {
676
+ when (index) {
677
+ 0 -> BottomSheetBehavior.STATE_COLLAPSED
678
+ 1 -> BottomSheetBehavior.STATE_EXPANDED
679
+ else -> BottomSheetBehavior.STATE_HIDDEN
680
+ }
681
+ }
682
+
683
+ 3 -> {
684
+ when (index) {
685
+ 0 -> BottomSheetBehavior.STATE_COLLAPSED
686
+ 1 -> BottomSheetBehavior.STATE_HALF_EXPANDED
687
+ 2 -> BottomSheetBehavior.STATE_EXPANDED
688
+ else -> BottomSheetBehavior.STATE_HIDDEN
689
+ }
690
+ }
691
+
692
+ else -> BottomSheetBehavior.STATE_HIDDEN
693
+ }
694
+
695
+ /**
696
+ * Get the DetentInfo data by state.
697
+ */
698
+ fun getDetentInfoForState(state: Int): DetentInfo? =
699
+ when (detents.size) {
700
+ 1 -> {
701
+ when (state) {
702
+ BottomSheetBehavior.STATE_COLLAPSED -> DetentInfo(0, getPositionForDetentIndex(0))
703
+ BottomSheetBehavior.STATE_EXPANDED -> DetentInfo(0, getPositionForDetentIndex(0))
704
+ else -> null
705
+ }
706
+ }
707
+
708
+ 2 -> {
709
+ when (state) {
710
+ BottomSheetBehavior.STATE_COLLAPSED -> DetentInfo(0, getPositionForDetentIndex(0))
711
+ BottomSheetBehavior.STATE_EXPANDED -> DetentInfo(1, getPositionForDetentIndex(1))
712
+ else -> null
713
+ }
714
+ }
715
+
716
+ 3 -> {
717
+ when (state) {
718
+ BottomSheetBehavior.STATE_COLLAPSED -> DetentInfo(0, getPositionForDetentIndex(0))
719
+ BottomSheetBehavior.STATE_HALF_EXPANDED -> DetentInfo(1, getPositionForDetentIndex(1))
720
+ BottomSheetBehavior.STATE_EXPANDED -> DetentInfo(2, getPositionForDetentIndex(2))
721
+ else -> null
722
+ }
723
+ }
724
+
725
+ else -> null
726
+ }
727
+
728
+ /**
729
+ * Calculate the expected Y position for a given detent index.
730
+ * Uses actual screen position if available, otherwise calculates based on screen height.
731
+ */
732
+ private fun getPositionForDetentIndex(index: Int): Float {
733
+ if (index < 0 || index >= detents.size) {
734
+ return 0f
735
+ }
736
+
737
+ // Try to get actual position from bottom sheet view first (same view used in behavior callbacks)
738
+ bottomSheetView?.let {
739
+ it
740
+ val screenY = ScreenUtils.getScreenY(it)
741
+ // Only use actual position if sheet has been positioned (screenY > 0)
742
+ if (screenY > 0) {
743
+ return screenY.pxToDp()
744
+ }
745
+ }
746
+
747
+ // Fallback: calculate expected position
748
+ val detentHeight = getDetentHeight(detents[index])
749
+
750
+ // Position calculation is simple: screen height - sheet height
751
+ // In both edge-to-edge and non-edge-to-edge modes, getScreenY returns
752
+ // coordinates in screen space, and maxScreenHeight represents the available height
753
+ // for the sheet, so the calculation is the same
754
+ val positionPx = maxScreenHeight - detentHeight
755
+
756
+ return positionPx.pxToDp()
757
+ }
758
+
759
+ /**
760
+ * Get DetentInfo data for given detent index.
761
+ */
762
+ fun getDetentInfoForIndex(index: Int) = getDetentInfoForState(getStateForDetentIndex(index)) ?: DetentInfo(0, 0f)
763
+
764
+ // ==================== RootView Implementation ====================
765
+
766
+ override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
767
+ super.onInitializeAccessibilityNodeInfo(info)
768
+
769
+ val testId = getTag(R.id.react_test_id) as String?
770
+ if (testId != null) {
771
+ info.viewIdResourceName = testId
772
+ }
773
+ }
774
+
775
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
776
+ super.onSizeChanged(w, h, oldw, oldh)
777
+
778
+ // Only proceed if size actually changed
779
+ if (w == oldw && h == oldh) return
780
+
781
+ // Update screen height based on new dimensions
782
+ val oldMaxScreenHeight = maxScreenHeight
783
+ maxScreenHeight = ScreenUtils.getScreenHeight(reactContext, edgeToEdgeEnabled)
784
+
785
+ // Only handle rotation if sheet is presented and screen height actually changed
786
+ if (isPresented && oldMaxScreenHeight != maxScreenHeight && oldMaxScreenHeight > 0) {
787
+ // Recalculate sheet detents with new screen dimensions
788
+ setupSheetDetents()
789
+
790
+ this.post {
791
+ // Update footer position after rotation
792
+ positionFooter()
793
+
794
+ // Notify JS about position change after rotation settles
795
+ val detentInfo = getDetentInfoForIndex(currentDetentIndex)
796
+ delegate?.viewControllerDidChangePosition(detentInfo.index, detentInfo.position, transitioning = true)
797
+ }
798
+ }
799
+
800
+ // Notify delegate about size change
801
+ delegate?.viewControllerDidChangeSize(w, h)
802
+ }
803
+
804
+ override fun handleException(t: Throwable) {
805
+ reactContext.reactApplicationContext.handleException(RuntimeException(t))
806
+ }
807
+
808
+ override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
809
+ eventDispatcher?.let { eventDispatcher ->
810
+ jSTouchDispatcher.handleTouchEvent(event, eventDispatcher, reactContext)
811
+ jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, true)
812
+ }
813
+ return super.onInterceptTouchEvent(event)
814
+ }
815
+
816
+ override fun onTouchEvent(event: MotionEvent): Boolean {
817
+ eventDispatcher?.let { eventDispatcher ->
818
+ jSTouchDispatcher.handleTouchEvent(event, eventDispatcher, reactContext)
819
+ jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, false)
820
+ }
821
+ super.onTouchEvent(event)
822
+ // In case when there is no children interested in handling touch event, we return true from
823
+ // the root view in order to receive subsequent events related to that gesture
824
+ return true
825
+ }
826
+
827
+ override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
828
+ eventDispatcher?.let { jSPointerDispatcher?.handleMotionEvent(event, it, true) }
829
+ return super.onHoverEvent(event)
830
+ }
831
+
832
+ override fun onHoverEvent(event: MotionEvent): Boolean {
833
+ eventDispatcher?.let { jSPointerDispatcher?.handleMotionEvent(event, it, false) }
834
+ return super.onHoverEvent(event)
835
+ }
836
+
837
+ @OptIn(UnstableReactNativeAPI::class)
838
+ @Suppress("DEPRECATION")
839
+ override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
840
+ eventDispatcher?.let { eventDispatcher ->
841
+ jSTouchDispatcher.onChildStartedNativeGesture(ev, eventDispatcher, reactContext)
842
+ jSPointerDispatcher?.onChildStartedNativeGesture(childView, ev, eventDispatcher)
843
+ }
844
+ }
845
+
846
+ override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
847
+ eventDispatcher?.let { jSTouchDispatcher.onChildEndedNativeGesture(ev, it) }
848
+ jSPointerDispatcher?.onChildEndedNativeGesture()
849
+ }
850
+
851
+ override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
852
+ // Allow the request to propagate to parent
853
+ super.requestDisallowInterceptTouchEvent(disallowIntercept)
854
+ }
855
+ }