@lodev09/react-native-true-sheet 3.0.0-beta.1 → 3.0.0-beta.10

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 (59) hide show
  1. package/README.md +7 -21
  2. package/RNTrueSheet.podspec +5 -1
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +58 -60
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +10 -18
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +76 -20
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetHeaderView.kt +38 -0
  7. package/android/src/main/java/com/lodev09/truesheet/TrueSheetHeaderViewManager.kt +21 -0
  8. package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +1 -0
  9. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +104 -146
  10. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +315 -403
  11. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +24 -11
  12. package/android/src/main/java/com/lodev09/truesheet/core/RNScreensFragmentObserver.kt +116 -0
  13. package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +33 -5
  14. package/android/src/main/jni/CMakeLists.txt +87 -0
  15. package/android/src/main/jni/TrueSheetSpec.h +17 -0
  16. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h +24 -0
  17. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +46 -0
  18. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.h +28 -0
  19. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.cpp +11 -0
  20. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.h +42 -0
  21. package/ios/TrueSheetContainerView.h +23 -0
  22. package/ios/TrueSheetContainerView.mm +80 -13
  23. package/ios/TrueSheetContentView.h +9 -1
  24. package/ios/TrueSheetContentView.mm +102 -24
  25. package/ios/TrueSheetFooterView.mm +2 -2
  26. package/ios/TrueSheetHeaderView.h +29 -0
  27. package/ios/TrueSheetHeaderView.mm +60 -0
  28. package/ios/TrueSheetView.h +0 -16
  29. package/ios/TrueSheetView.mm +208 -194
  30. package/ios/TrueSheetViewController.h +4 -3
  31. package/ios/TrueSheetViewController.mm +180 -272
  32. package/ios/utils/ConversionUtil.h +24 -0
  33. package/ios/utils/ConversionUtil.mm +50 -0
  34. package/ios/utils/LayoutUtil.h +11 -1
  35. package/ios/utils/LayoutUtil.mm +32 -1
  36. package/lib/module/TrueSheet.js +30 -33
  37. package/lib/module/TrueSheet.js.map +1 -1
  38. package/lib/module/fabric/TrueSheetContainerViewNativeComponent.ts +1 -1
  39. package/lib/module/fabric/TrueSheetHeaderViewNativeComponent.ts +8 -0
  40. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +6 -8
  41. package/lib/typescript/src/TrueSheet.d.ts +0 -3
  42. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  43. package/lib/typescript/src/TrueSheet.types.d.ts +30 -43
  44. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  45. package/lib/typescript/src/fabric/TrueSheetContainerViewNativeComponent.d.ts.map +1 -1
  46. package/lib/typescript/src/fabric/TrueSheetHeaderViewNativeComponent.d.ts +6 -0
  47. package/lib/typescript/src/fabric/TrueSheetHeaderViewNativeComponent.d.ts.map +1 -0
  48. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +3 -6
  49. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  50. package/package.json +9 -5
  51. package/react-native.config.js +5 -2
  52. package/src/TrueSheet.tsx +34 -31
  53. package/src/TrueSheet.types.ts +47 -61
  54. package/src/fabric/TrueSheetContainerViewNativeComponent.ts +1 -1
  55. package/src/fabric/TrueSheetHeaderViewNativeComponent.ts +8 -0
  56. package/src/fabric/TrueSheetViewNativeComponent.ts +6 -8
  57. package/android/src/main/java/com/lodev09/truesheet/events/SizeChangeEvent.kt +0 -27
  58. package/ios/events/OnSizeChangeEvent.h +0 -28
  59. package/ios/events/OnSizeChangeEvent.mm +0 -30
@@ -6,6 +6,9 @@ import android.view.ViewStructure
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
+ import com.facebook.react.uimanager.PixelUtil.pxToDp
11
+ import com.facebook.react.uimanager.StateWrapper
9
12
  import com.facebook.react.uimanager.ThemedReactContext
10
13
  import com.facebook.react.uimanager.UIManagerHelper
11
14
  import com.facebook.react.uimanager.events.EventDispatcher
@@ -18,13 +21,12 @@ import com.lodev09.truesheet.events.DragChangeEvent
18
21
  import com.lodev09.truesheet.events.DragEndEvent
19
22
  import com.lodev09.truesheet.events.MountEvent
20
23
  import com.lodev09.truesheet.events.PositionChangeEvent
21
- import com.lodev09.truesheet.events.SizeChangeEvent
22
24
  import com.lodev09.truesheet.events.WillDismissEvent
23
25
  import com.lodev09.truesheet.events.WillPresentEvent
24
26
 
25
27
  /**
26
- * Main TrueSheet host view.
27
- * Manages the sheet dialog and container, and dispatches events to JavaScript.
28
+ * Main TrueSheet host view that manages the sheet dialog and dispatches events to JavaScript.
29
+ * This view is hidden (GONE) and delegates all rendering to TrueSheetViewController in a dialog window.
28
30
  */
29
31
  @SuppressLint("ViewConstructor")
30
32
  class TrueSheetView(private val reactContext: ThemedReactContext) :
@@ -33,14 +35,8 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
33
35
  TrueSheetViewControllerDelegate,
34
36
  TrueSheetContainerViewDelegate {
35
37
 
36
- /**
37
- * The TrueSheetViewController instance that acts as both root view and controller
38
- */
39
38
  private val viewController: TrueSheetViewController = TrueSheetViewController(reactContext)
40
39
 
41
- /**
42
- * Gets the container view (first child of view controller)
43
- */
44
40
  private val containerView: TrueSheetContainerView?
45
41
  get() = viewController.getChildAt(0) as? TrueSheetContainerView
46
42
 
@@ -49,19 +45,28 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
49
45
  var initialDetentIndex: Int = -1
50
46
  var initialDetentAnimated: Boolean = true
51
47
 
52
- /**
53
- * Tracks if initial presentation has been handled
54
- */
55
- private var hasHandledInitialPresentation = false
48
+ var stateWrapper: StateWrapper? = null
49
+ set(value) {
50
+ // Immediately update state with screen width during first state update
51
+ // This ensures we have initial width for content layout before presenting
52
+ if (field == null && value != null) {
53
+ updateState(viewController.screenWidth, 0)
54
+ }
55
+ field = value
56
+ }
57
+
58
+ // Track last dimensions to avoid unnecessary state updates
59
+ private var lastContainerWidth: Int = 0
60
+ private var lastContainerHeight: Int = 0
61
+
62
+ // Flag to prevent multiple pending sheet updates
63
+ private var isSheetUpdatePending: Boolean = false
56
64
 
57
65
  init {
58
66
  reactContext.addLifecycleEventListener(this)
59
-
60
- // Set delegates
61
67
  viewController.delegate = this
62
68
 
63
- // Hide the host view from layout and touch handling
64
- // The actual content is shown in a dialog window
69
+ // Hide the host view - actual content is rendered in the dialog window
65
70
  visibility = GONE
66
71
  }
67
72
 
@@ -81,102 +86,66 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
81
86
 
82
87
  override fun setId(id: Int) {
83
88
  super.setId(id)
84
-
85
89
  viewController.id = id
86
90
  TrueSheetModule.registerView(this, id)
87
91
  }
88
92
 
89
- override fun onAttachedToWindow() {
90
- super.onAttachedToWindow()
91
- }
92
-
93
93
  override fun onDetachedFromWindow() {
94
94
  super.onDetachedFromWindow()
95
- onDropInstance()
96
95
 
97
- TrueSheetModule.unregisterView(id)
96
+ // Don't unregister if we have active modals - we need the view for recovery
97
+ if (viewController.hasActiveModals()) {
98
+ return
99
+ }
100
+
101
+ onDropInstance()
98
102
  }
99
103
 
100
104
  /**
101
- * showOrUpdate will display the Dialog. It is called by the manager once all properties are set
102
- * because we need to know all of them before creating the Dialog. It is also smart during updates
103
- * if the changed properties can be applied directly to the Dialog or require the recreation of a
104
- * new Dialog.
105
+ * Called by the manager after all properties are set.
106
+ * Reconfigures the sheet if it's currently presented.
105
107
  */
106
- fun showOrUpdate() {
107
- // Only handle initial presentation once on mount
108
- if (!hasHandledInitialPresentation && initialDetentIndex >= 0) {
109
- hasHandledInitialPresentation = true
110
-
111
- // Create dialog if not created yet
112
- if (!viewController.isPresented) {
113
- viewController.createDialog()
114
- }
115
-
116
- post {
117
- present(initialDetentIndex, initialDetentAnimated) { }
118
- }
119
- } else if (viewController.isPresented) {
120
- viewController.setupSheetDetents()
108
+ fun finalizeUpdates() {
109
+ if (viewController.isPresented) {
110
+ viewController.setupBackground()
111
+ viewController.setupGrabber()
112
+ updateSheetIfNeeded()
121
113
  viewController.setStateForDetentIndex(viewController.currentDetentIndex)
122
- viewController.positionFooter()
123
114
  }
124
115
  }
125
116
 
126
117
  // ==================== View Management ====================
127
118
 
128
119
  override fun addView(child: View?, index: Int) {
129
- // Add the child to our ViewController
130
- // This is the TrueSheetContainerView
131
120
  viewController.addView(child, index)
132
121
 
133
- // Create dialog and dispatch mount event when TrueSheetContainerView is added
134
122
  if (child is TrueSheetContainerView) {
135
- // Set up container delegate to listen for content size changes
136
123
  child.delegate = this
124
+ viewController.createDialog()
137
125
 
138
- // Get initial content height from container
139
- val contentHeight = child.contentHeight
140
- if (contentHeight > 0) {
141
- viewController.contentHeight = contentHeight
126
+ // Present at initial detent after layout pass when content height is available
127
+ if (initialDetentIndex >= 0) {
128
+ post { present(initialDetentIndex, initialDetentAnimated) { } }
142
129
  }
143
130
 
144
- // Create the dialog now that the container is mounted
145
- viewController.createDialog()
146
-
147
131
  val surfaceId = UIManagerHelper.getSurfaceId(this)
148
- eventDispatcher?.dispatchEvent(
149
- MountEvent(surfaceId, id)
150
- )
132
+ eventDispatcher?.dispatchEvent(MountEvent(surfaceId, id))
151
133
  }
152
134
  }
153
135
 
154
136
  override fun getChildCount(): Int = viewController.childCount
155
137
  override fun getChildAt(index: Int): View? = viewController.getChildAt(index)
156
138
 
157
- override fun removeView(child: View?) {
158
- if (child != null) {
159
- // Clean up container delegate
160
- if (child is TrueSheetContainerView) {
161
- child.delegate = null
162
- }
163
-
164
- viewController.removeView(child)
165
- }
166
- }
167
-
168
139
  override fun removeViewAt(index: Int) {
169
140
  val child = getChildAt(index)
141
+ if (child is TrueSheetContainerView) {
142
+ child.delegate = null
143
+ }
170
144
  viewController.removeView(child)
171
145
  }
172
146
 
173
- override fun addChildrenForAccessibility(outChildren: ArrayList<View>) {
174
- // Explicitly override this to prevent accessibility events being passed down to children
175
- // Those will be handled by the mHostView which lives in the dialog
176
- }
177
-
178
- // Explicitly override this to prevent accessibility events being passed down to children
179
- // Those will be handled by the mHostView which lives in the dialog
147
+ // Accessibility events are handled by the dialog's host view
148
+ override fun addChildrenForAccessibility(outChildren: ArrayList<View>) {}
180
149
  override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent): Boolean = false
181
150
 
182
151
  fun onDropInstance() {
@@ -190,7 +159,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
190
159
  }
191
160
 
192
161
  override fun onHostResume() {
193
- showOrUpdate()
162
+ finalizeUpdates()
194
163
  }
195
164
 
196
165
  override fun onHostPause() {
@@ -204,71 +173,59 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
204
173
 
205
174
  override fun viewControllerWillPresent(index: Int, position: Float) {
206
175
  val surfaceId = UIManagerHelper.getSurfaceId(this)
207
- eventDispatcher?.dispatchEvent(
208
- WillPresentEvent(surfaceId, id, index, position)
209
- )
176
+ eventDispatcher?.dispatchEvent(WillPresentEvent(surfaceId, id, index, position))
210
177
  }
211
178
 
212
179
  override fun viewControllerDidPresent(index: Int, position: Float) {
213
180
  val surfaceId = UIManagerHelper.getSurfaceId(this)
214
- eventDispatcher?.dispatchEvent(
215
- DidPresentEvent(surfaceId, id, index, position)
216
- )
181
+ eventDispatcher?.dispatchEvent(DidPresentEvent(surfaceId, id, index, position))
217
182
 
218
- // Set our touch event dispatcher on the view controller
183
+ // Enable touch event dispatching to React Native
219
184
  viewController.eventDispatcher = eventDispatcher
185
+ containerView?.footerView?.eventDispatcher = eventDispatcher
220
186
  }
221
187
 
222
188
  override fun viewControllerWillDismiss() {
223
189
  val surfaceId = UIManagerHelper.getSurfaceId(this)
224
- eventDispatcher?.dispatchEvent(
225
- WillDismissEvent(surfaceId, id)
226
- )
190
+ eventDispatcher?.dispatchEvent(WillDismissEvent(surfaceId, id))
227
191
 
228
- // Clear our touch event dispatcher on the view controller
192
+ // Disable touch event dispatching
229
193
  viewController.eventDispatcher = null
194
+ containerView?.footerView?.eventDispatcher = null
230
195
  }
231
196
 
232
197
  override fun viewControllerDidDismiss() {
233
198
  val surfaceId = UIManagerHelper.getSurfaceId(this)
234
- eventDispatcher?.dispatchEvent(
235
- DidDismissEvent(surfaceId, id)
236
- )
199
+ eventDispatcher?.dispatchEvent(DidDismissEvent(surfaceId, id))
237
200
  }
238
201
 
239
202
  override fun viewControllerDidChangeDetent(index: Int, position: Float) {
240
203
  val surfaceId = UIManagerHelper.getSurfaceId(this)
241
- eventDispatcher?.dispatchEvent(
242
- DetentChangeEvent(surfaceId, id, index, position)
243
- )
204
+ eventDispatcher?.dispatchEvent(DetentChangeEvent(surfaceId, id, index, position))
244
205
  }
245
206
 
246
207
  override fun viewControllerDidDragBegin(index: Int, position: Float) {
247
208
  val surfaceId = UIManagerHelper.getSurfaceId(this)
248
- eventDispatcher?.dispatchEvent(
249
- DragBeginEvent(surfaceId, id, index, position)
250
- )
209
+ eventDispatcher?.dispatchEvent(DragBeginEvent(surfaceId, id, index, position))
251
210
  }
252
211
 
253
212
  override fun viewControllerDidDragChange(index: Int, position: Float) {
254
213
  val surfaceId = UIManagerHelper.getSurfaceId(this)
255
- eventDispatcher?.dispatchEvent(
256
- DragChangeEvent(surfaceId, id, index, position)
257
- )
214
+ eventDispatcher?.dispatchEvent(DragChangeEvent(surfaceId, id, index, position))
258
215
  }
259
216
 
260
217
  override fun viewControllerDidDragEnd(index: Int, position: Float) {
261
218
  val surfaceId = UIManagerHelper.getSurfaceId(this)
262
- eventDispatcher?.dispatchEvent(
263
- DragEndEvent(surfaceId, id, index, position)
264
- )
219
+ eventDispatcher?.dispatchEvent(DragEndEvent(surfaceId, id, index, position))
265
220
  }
266
221
 
267
222
  override fun viewControllerDidChangePosition(index: Int, position: Float, transitioning: Boolean) {
268
223
  val surfaceId = UIManagerHelper.getSurfaceId(this)
269
- eventDispatcher?.dispatchEvent(
270
- PositionChangeEvent(surfaceId, id, index, position, transitioning)
271
- )
224
+ eventDispatcher?.dispatchEvent(PositionChangeEvent(surfaceId, id, index, position, transitioning))
225
+ }
226
+
227
+ override fun viewControllerDidChangeSize(width: Int, height: Int) {
228
+ updateState(width, height)
272
229
  }
273
230
 
274
231
  // ==================== Property Setters (forward to controller) ====================
@@ -286,24 +243,22 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
286
243
  }
287
244
  }
288
245
 
289
- fun setDimmedIndex(index: Int) {
290
- if (viewController.dimmedIndex == index) return
291
- viewController.dimmedIndex = index
246
+ fun setDimmedDetentIndex(index: Int) {
247
+ if (viewController.dimmedDetentIndex == index) return
248
+ viewController.dimmedDetentIndex = index
292
249
  if (viewController.isPresented) {
293
250
  viewController.setupDimmedBackground(viewController.currentDetentIndex)
294
251
  }
295
252
  }
296
253
 
297
254
  fun setCornerRadius(radius: Float) {
298
- if (viewController.cornerRadius == radius) return
299
- viewController.cornerRadius = radius
300
- viewController.setupBackground()
255
+ if (viewController.sheetCornerRadius == radius) return
256
+ viewController.sheetCornerRadius = radius
301
257
  }
302
258
 
303
259
  fun setSheetBackgroundColor(color: Int) {
304
260
  if (viewController.sheetBackgroundColor == color) return
305
261
  viewController.sheetBackgroundColor = color
306
- viewController.setupBackground()
307
262
  }
308
263
 
309
264
  fun setSoftInputMode(mode: Int) {
@@ -314,7 +269,9 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
314
269
  viewController.dismissible = dismissible
315
270
  }
316
271
 
317
- fun setGrabber(grabber: Boolean) {}
272
+ fun setGrabber(grabber: Boolean) {
273
+ viewController.grabber = grabber
274
+ }
318
275
 
319
276
  fun setDetents(newDetents: MutableList<Double>) {
320
277
  viewController.detents = newDetents
@@ -326,61 +283,62 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
326
283
  viewController.edgeToEdgeFullScreen = edgeToEdgeFullScreen
327
284
  }
328
285
 
286
+ // ==================== State Management ====================
287
+
329
288
  /**
330
- * Presents the sheet at the given detent index.
331
- *
332
- * @param detentIndex The detent index to present at
333
- * @param animated Whether to animate the presentation
334
- * @param promiseCallback Callback invoked when presentation completes
289
+ * Updates the Fabric state with container dimensions for Yoga layout.
335
290
  */
291
+ fun updateState(width: Int, height: Int) {
292
+ if (width == lastContainerWidth && height == lastContainerHeight) return
293
+
294
+ lastContainerWidth = width
295
+ lastContainerHeight = height
296
+
297
+ val sw = stateWrapper ?: return
298
+ val newStateData = WritableNativeMap()
299
+ newStateData.putDouble("containerWidth", width.toFloat().pxToDp().toDouble())
300
+ newStateData.putDouble("containerHeight", height.toFloat().pxToDp().toDouble())
301
+ sw.updateState(newStateData)
302
+ }
303
+
336
304
  @UiThread
337
305
  fun present(detentIndex: Int, animated: Boolean = true, promiseCallback: () -> Unit) {
338
306
  viewController.presentPromise = promiseCallback
339
307
  viewController.present(detentIndex, animated)
340
308
  }
341
309
 
342
- /**
343
- * Dismisses the sheet.
344
- *
345
- * @param promiseCallback Callback invoked when dismissal completes
346
- */
347
310
  @UiThread
348
311
  fun dismiss(promiseCallback: () -> Unit) {
349
312
  viewController.dismissPromise = promiseCallback
350
313
  viewController.dismiss()
351
314
  }
352
315
 
353
- override fun viewControllerDidChangeSize(width: Int, height: Int) {
354
- // Dispatch size change event to JS
355
- val surfaceId = UIManagerHelper.getSurfaceId(this)
356
- eventDispatcher?.dispatchEvent(
357
- SizeChangeEvent(surfaceId, id, width, height)
358
- )
316
+ /**
317
+ * Debounced sheet update to handle rapid content/header size changes.
318
+ * Uses post to ensure all layout passes complete before reconfiguring.
319
+ */
320
+ fun updateSheetIfNeeded() {
321
+ if (!viewController.isPresented || isSheetUpdatePending) return
322
+
323
+ isSheetUpdatePending = true
324
+ viewController.post {
325
+ isSheetUpdatePending = false
326
+ viewController.setupSheetDetents()
327
+ viewController.positionFooter()
328
+ }
359
329
  }
360
330
 
361
331
  // ==================== TrueSheetContainerViewDelegate Implementation ====================
362
332
 
363
333
  override fun containerViewContentDidChangeSize(width: Int, height: Int) {
364
- // Clamp content height to container height to prevent unbounded growth with scrollable content
365
- val containerHeight = viewController.maxScreenHeight
366
- val contentHeight = if (containerHeight > 0) minOf(height, containerHeight) else height
367
-
368
- viewController.contentHeight = contentHeight
369
-
370
- // Update detents if sheet is already presented
371
- if (viewController.isPresented) {
372
- // Reconfigure sheet detents with new content height
373
- viewController.setupSheetDetents()
334
+ updateSheetIfNeeded()
335
+ }
374
336
 
375
- // Use post to ensure layout is complete before positioning footer
376
- viewController.post {
377
- viewController.positionFooter()
378
- }
379
- }
337
+ override fun containerViewHeaderDidChangeSize(width: Int, height: Int) {
338
+ updateSheetIfNeeded()
380
339
  }
381
340
 
382
341
  override fun containerViewFooterDidChangeSize(width: Int, height: Int) {
383
- // Reposition footer when its size changes
384
342
  if (viewController.isPresented) {
385
343
  viewController.positionFooter()
386
344
  }