@lodev09/react-native-true-sheet 3.1.0-beta.7 → 3.1.0-beta.8

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.
@@ -22,10 +22,6 @@ class TrueSheetModule(reactContext: ReactApplicationContext) :
22
22
 
23
23
  override fun getName(): String = NAME
24
24
 
25
- override fun initialize() {
26
- super.initialize()
27
- }
28
-
29
25
  override fun invalidate() {
30
26
  super.invalidate()
31
27
  // Clear all registered views and observer on module invalidation
@@ -170,48 +170,26 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
170
170
  val statusBarHeight: Int
171
171
  get() = ScreenUtils.getStatusBarHeight(reactContext)
172
172
 
173
- /**
174
- * The bottom inset (navigation bar height) to add to sheet height.
175
- * This matches iOS behavior where the system adds bottom safe area inset internally.
176
- */
173
+ /** Navigation bar height, added to sheet height to match iOS behavior. */
177
174
  private val bottomInset: Int
178
175
  get() = ScreenUtils.getNavigationBarHeight(reactContext)
179
176
 
180
- /**
181
- * Edge-to-edge is enabled by default on API 36+, or when explicitly configured.
182
- */
177
+ /** Edge-to-edge enabled by default on API 36+, or when explicitly configured. */
183
178
  private val edgeToEdgeEnabled: Boolean
184
179
  get() {
185
180
  val defaultEnabled = android.os.Build.VERSION.SDK_INT >= 36
186
181
  return BuildConfig.EDGE_TO_EDGE_ENABLED || dialog?.edgeToEdgeEnabled == true || defaultEnabled
187
182
  }
188
183
 
189
- /**
190
- * The top inset to apply when edge-to-edge is enabled but not full-screen.
191
- * This prevents the sheet from going under the status bar.
192
- */
184
+ /** Top inset when edge-to-edge is enabled but not full-screen. */
193
185
  private val sheetTopInset: Int
194
186
  get() = if (edgeToEdgeEnabled && !edgeToEdgeFullScreen) statusBarHeight else 0
195
187
 
196
- // ====================================================================
197
- // MARK: - Touch Dispatchers
198
- // ====================================================================
199
-
200
- /**
201
- * Touch dispatchers are required for RootView to properly forward touch events to React Native.
202
- */
203
188
  internal var eventDispatcher: EventDispatcher? = null
204
189
  private val jSTouchDispatcher = JSTouchDispatcher(this)
205
190
  private var jSPointerDispatcher: JSPointerDispatcher? = null
206
191
 
207
- // ====================================================================
208
- // MARK: - Modal Observer
209
- // ====================================================================
210
-
211
- /**
212
- * Observes react-native-screens modal fragments to hide/show the sheet appropriately.
213
- * This prevents the sheet from rendering on top of modals.
214
- */
192
+ /** Hides/shows the sheet when RN Screens modals are presented/dismissed. */
215
193
  private var rnScreensObserver: RNScreensFragmentObserver? = null
216
194
 
217
195
  // ====================================================================
@@ -253,7 +231,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
253
231
  behavior.isHideable = dismissible
254
232
  behavior.isDraggable = draggable
255
233
 
256
- // Handle back press
257
234
  onBackPressedDispatcher.addCallback(object : androidx.activity.OnBackPressedCallback(true) {
258
235
  override fun handleOnBackPressed() {
259
236
  this@TrueSheetViewController.delegate?.viewControllerDidBackPress()
@@ -293,25 +270,17 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
293
270
 
294
271
  sheetContainer?.post {
295
272
  val positionPx = bottomSheetView?.let { ScreenUtils.getScreenY(it) } ?: screenHeight
296
-
297
- // Store resolved position for initial detent
298
273
  storeResolvedPosition(currentDetentIndex)
299
274
  emitChangePositionDelegate(positionPx, realtime = false)
300
-
301
275
  positionFooter()
302
276
  }
303
277
 
304
- // Emit didPresent/didFocus after present animation completes
305
278
  sheetContainer?.postDelayed({
306
279
  val detentInfo = getDetentInfoForIndex(currentDetentIndex)
307
280
  val detent = getDetentValueForIndex(detentInfo.index)
308
281
 
309
282
  delegate?.viewControllerDidPresent(detentInfo.index, detentInfo.position, detent)
310
-
311
- // Notify parent sheet that it has lost focus (after this sheet appeared)
312
283
  parentSheetView?.viewControllerDidBlur()
313
-
314
- // Emit didFocus with didPresent
315
284
  delegate?.viewControllerDidFocus()
316
285
 
317
286
  presentPromise?.invoke()
@@ -320,25 +289,19 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
320
289
  }
321
290
 
322
291
  dialog.setOnCancelListener {
323
- // Skip if already handled by STATE_HIDDEN (programmatic dismiss)
324
292
  if (isDismissing) return@setOnCancelListener
325
293
 
326
- // User-initiated dismiss (back button, tap outside)
327
294
  isDismissing = true
328
295
  emitWillDismissEvents()
329
-
330
- // Emit off-screen position since onSlide isn't triggered for user-initiated dismiss
331
296
  emitChangePositionDelegate(screenHeight, realtime = false)
297
+ }
332
298
 
333
- // Emit didBlur/didDismiss after dismiss animation completes
299
+ dialog.setOnDismissListener {
334
300
  android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
335
301
  emitDidDismissEvents()
302
+ cleanupDialog()
336
303
  }, DISMISS_ANIMATION_DURATION)
337
304
  }
338
-
339
- dialog.setOnDismissListener {
340
- cleanupDialog()
341
- }
342
305
  }
343
306
 
344
307
  private fun setupBottomSheetBehavior(dialog: BottomSheetDialog) {
@@ -362,9 +325,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
362
325
 
363
326
  override fun onStateChanged(sheetView: View, newState: Int) {
364
327
  if (newState == BottomSheetBehavior.STATE_HIDDEN) {
365
- // Mark as dismissing so setOnCancelListener skips emitting events
328
+ if (isDismissing) return
366
329
  isDismissing = true
367
- emitDidDismissEvents()
330
+ emitWillDismissEvents()
368
331
  dialog.dismiss()
369
332
  return
370
333
  }
@@ -377,15 +340,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
377
340
  BottomSheetBehavior.STATE_EXPANDED,
378
341
  BottomSheetBehavior.STATE_COLLAPSED,
379
342
  BottomSheetBehavior.STATE_HALF_EXPANDED -> {
380
- // Ignore state changes triggered by content size reconfiguration
381
343
  if (isReconfiguring) return
382
344
 
383
345
  getDetentInfoForState(newState)?.let { detentInfo ->
384
- // Store resolved position when sheet settles
385
346
  storeResolvedPosition(detentInfo.index)
386
347
 
387
348
  if (isDragging) {
388
- // Handle drag end
389
349
  val detent = getDetentValueForIndex(detentInfo.index)
390
350
  delegate?.viewControllerDidDragEnd(detentInfo.index, detentInfo.position, detent)
391
351
 
@@ -398,13 +358,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
398
358
  }
399
359
 
400
360
  isDragging = false
401
- } else {
402
- // Handle programmatic resize - emit detent change after sheet settles
403
- if (detentInfo.index != currentDetentIndex) {
404
- val detent = getDetentValueForIndex(detentInfo.index)
405
- currentDetentIndex = detentInfo.index
406
- delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
407
- }
361
+ } else if (detentInfo.index != currentDetentIndex) {
362
+ val detent = getDetentValueForIndex(detentInfo.index)
363
+ currentDetentIndex = detentInfo.index
364
+ delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
408
365
  }
409
366
  }
410
367
  }
@@ -450,22 +407,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
450
407
  rnScreensObserver = null
451
408
  }
452
409
 
453
- // ====================================================================
454
- // MARK: - Dismiss Event Helpers
455
- // ====================================================================
456
-
457
- /**
458
- * Emits willBlur and willDismiss events, and notifies parent sheet of upcoming focus change.
459
- */
460
410
  private fun emitWillDismissEvents() {
461
411
  delegate?.viewControllerWillBlur()
462
412
  delegate?.viewControllerWillDismiss()
463
413
  parentSheetView?.viewControllerWillFocus()
464
414
  }
465
415
 
466
- /**
467
- * Emits didBlur and didDismiss events, notifies parent sheet, and invokes dismiss promise.
468
- */
469
416
  private fun emitDidDismissEvents() {
470
417
  val hadParent = parentSheetView != null
471
418
  parentSheetView?.viewControllerDidFocus()
@@ -482,53 +429,32 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
482
429
  // MARK: - Dialog Visibility (for stacking)
483
430
  // ====================================================================
484
431
 
485
- /**
486
- * Returns true if the sheet's top is at or above the status bar.
487
- */
488
432
  val isExpanded: Boolean
489
433
  get() {
490
434
  val sheetTop = bottomSheetView?.top ?: return false
491
435
  return sheetTop <= statusBarHeight
492
436
  }
493
437
 
494
- /**
495
- * Returns the current top position of the sheet (Y coordinate from screen top).
496
- * Used for comparing sheet positions during stacking.
497
- */
498
438
  val currentSheetTop: Int
499
439
  get() = bottomSheetView?.let { ScreenUtils.getScreenY(it) } ?: screenHeight
500
440
 
501
- /**
502
- * Returns the expected top position of the sheet when presented at the given detent index.
503
- * Used for comparing sheet positions before presentation.
504
- */
505
441
  fun getExpectedSheetTop(detentIndex: Int): Int {
506
442
  if (detentIndex < 0 || detentIndex >= detents.size) return screenHeight
507
443
  val detentHeight = getDetentHeight(detents[detentIndex])
508
444
  return screenHeight - detentHeight
509
445
  }
510
446
 
511
- /**
512
- * Hides the dialog without dismissing it.
513
- * Used when another TrueSheet presents on top or when RN screen is presented.
514
- */
447
+ /** Hides without dismissing. Used for sheet stacking and RN Screens modals. */
515
448
  fun hideDialog() {
516
449
  isDialogVisible = false
517
450
  dialog?.window?.decorView?.visibility = INVISIBLE
518
-
519
- // Emit off-screen position (detent = 0 since sheet is fully hidden)
520
451
  emitChangePositionDelegate(screenHeight, realtime = false)
521
452
  }
522
453
 
523
- /**
524
- * Shows a previously hidden dialog.
525
- * Used when the sheet on top dismisses.
526
- */
454
+ /** Shows a previously hidden dialog. */
527
455
  fun showDialog() {
528
456
  isDialogVisible = true
529
457
  dialog?.window?.decorView?.visibility = VISIBLE
530
-
531
- // Emit current position
532
458
  val positionPx = bottomSheetView?.let { ScreenUtils.getScreenY(it) } ?: screenHeight
533
459
  emitChangePositionDelegate(positionPx, realtime = false)
534
460
  }
@@ -546,8 +472,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
546
472
  setupDimmedBackground(detentIndex)
547
473
 
548
474
  if (isPresented) {
549
- // Detent change will be emitted when sheet settles in onStateChanged
550
- // Don't update currentDetentIndex here - it will be updated when sheet settles
551
475
  setStateForDetentIndex(detentIndex)
552
476
  } else {
553
477
  currentDetentIndex = detentIndex
@@ -558,12 +482,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
558
482
  val detentInfo = getDetentInfoForIndex(detentIndex)
559
483
  val detent = getDetentValueForIndex(detentInfo.index)
560
484
 
561
- // Notify parent sheet that it is about to lose focus (before this sheet appears)
562
485
  parentSheetView?.viewControllerWillBlur()
563
-
564
486
  delegate?.viewControllerWillPresent(detentInfo.index, detentInfo.position, detent)
565
-
566
- // Emit willFocus with willPresent
567
487
  delegate?.viewControllerWillFocus()
568
488
 
569
489
  if (!animated) {
@@ -575,10 +495,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
575
495
  }
576
496
 
577
497
  fun dismiss(animated: Boolean = true) {
498
+ if (isDismissing) return
499
+
500
+ isDismissing = true
578
501
  emitWillDismissEvents()
579
502
 
580
503
  this.post {
581
- // Emit off-screen position (detent = 0 since sheet is fully hidden)
582
504
  emitChangePositionDelegate(screenHeight, realtime = false)
583
505
  }
584
506
 
@@ -586,9 +508,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
586
508
  dialog?.window?.setWindowAnimations(0)
587
509
  }
588
510
 
589
- // Temporarily enable hideable to allow STATE_HIDDEN transition
590
- behavior?.isHideable = true
591
- behavior?.state = BottomSheetBehavior.STATE_HIDDEN
511
+ dialog?.dismiss()
592
512
  }
593
513
 
594
514
  // ====================================================================
@@ -598,13 +518,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
598
518
  fun setupSheetDetents() {
599
519
  val behavior = this.behavior ?: return
600
520
 
601
- // Reset resolved positions if detents count changed
602
521
  if (resolvedDetentPositions.size != detents.size) {
603
522
  resolvedDetentPositions.clear()
604
523
  repeat(detents.size) { resolvedDetentPositions.add(0) }
605
524
  }
606
525
 
607
- // Always update auto detent positions based on current content height
608
526
  for (i in detents.indices) {
609
527
  if (detents[i] == -1.0) {
610
528
  val detentHeight = getDetentHeight(detents[i])
@@ -612,7 +530,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
612
530
  }
613
531
  }
614
532
 
615
- // Flag to prevent state change callbacks from updating detent index during reconfiguration
616
533
  isReconfiguring = true
617
534
 
618
535
  behavior.apply {
@@ -643,7 +560,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
643
560
  }
644
561
  }
645
562
 
646
- // Keep container size in sync with sheet size
647
563
  if (oldExpandOffset != expandedOffset || expandedOffset == 0) {
648
564
  val offset = if (expandedOffset == 0) statusBarHeight else 0
649
565
  val newHeight = screenHeight - expandedOffset - offset
@@ -651,7 +567,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
651
567
  }
652
568
 
653
569
  if (isPresented) {
654
- // Re-apply current state to update position after config changes
655
570
  setStateForDetentIndex(currentDetentIndex)
656
571
  }
657
572
 
@@ -688,10 +603,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
688
603
  bottomSheet.clipToOutline = true
689
604
  }
690
605
 
691
- /**
692
- * Configures the dimmed background based on the current detent index.
693
- * When not dimmed, touch events pass through to the activity behind the sheet.
694
- */
606
+ /** Configures dim and touch-through behavior based on detent index. */
695
607
  fun setupDimmedBackground(detentIndex: Int) {
696
608
  val dialog = this.dialog ?: return
697
609
  dialog.window?.apply {
@@ -702,7 +614,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
702
614
  setFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND, WindowManager.LayoutParams.FLAG_DIM_BEHIND)
703
615
  dialog.setCanceledOnTouchOutside(dismissible)
704
616
  } else {
705
- // Forward touch events to the activity when not dimmed
706
617
  view.setOnTouchListener { v, event ->
707
618
  event.setLocation(event.rawX - v.x, event.rawY - v.y)
708
619
  reactContext.currentActivity?.dispatchTouchEvent(event)
@@ -718,11 +629,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
718
629
  dialog?.window?.setWindowAnimations(windowAnimation)
719
630
  }
720
631
 
721
- /**
722
- * Positions the footer view at the bottom of the sheet.
723
- * The footer stays fixed at the bottom edge of the visible sheet area,
724
- * adjusting during drag gestures via slideOffset.
725
- */
632
+ /** Positions footer at bottom of sheet, adjusting during drag via slideOffset. */
726
633
  fun positionFooter(slideOffset: Float? = null) {
727
634
  val footerView = containerView?.footerView ?: return
728
635
  val bottomSheet = bottomSheetView ?: return
@@ -732,13 +639,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
732
639
 
733
640
  var footerY = (screenHeight - bottomSheetY - footerHeight).toFloat()
734
641
 
735
- // Animate footer down with sheet when below peek height
736
642
  if (slideOffset != null && slideOffset < 0) {
737
643
  footerY -= (footerHeight * slideOffset)
738
644
  }
739
645
 
740
- // Clamp footer position to prevent it from going off screen when positioning at the top
741
- // This happens when fullScreen is enabled in edge-to-edge mode
742
646
  val maxAllowedY = (screenHeight - statusBarHeight - footerHeight).toFloat()
743
647
  footerView.y = minOf(footerY, maxAllowedY)
744
648
  }
@@ -766,14 +670,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
766
670
  }
767
671
 
768
672
  // ====================================================================
769
- // MARK: - Position Change Delegate
673
+ // MARK: - Position & Drag Handling
770
674
  // ====================================================================
771
675
 
772
- /**
773
- * Emits position change to the delegate if the position has changed.
774
- * @param positionPx The current position in pixels (screen Y coordinate)
775
- * @param realtime Whether the position is a real-time value (during drag or animation tracking)
776
- */
777
676
  private fun emitChangePositionDelegate(positionPx: Int, realtime: Boolean) {
778
677
  if (positionPx == lastEmittedPositionPx) return
779
678
 
@@ -784,11 +683,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
784
683
  delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, realtime)
785
684
  }
786
685
 
787
- /**
788
- * Stores the current Y position as the resolved position for the given detent index.
789
- * This is called when the sheet settles at a detent to capture the actual position
790
- * which may differ from the calculated position due to system adjustments.
791
- */
792
686
  private fun storeResolvedPosition(index: Int) {
793
687
  if (index < 0 || index >= resolvedDetentPositions.size) return
794
688
  val positionPx = bottomSheetView?.let { ScreenUtils.getScreenY(it) } ?: return
@@ -797,24 +691,16 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
797
691
  }
798
692
  }
799
693
 
800
- /**
801
- * Stores the resolved position for the current detent.
802
- * Called from TrueSheetView when content size changes.
803
- */
804
694
  fun storeCurrentResolvedPosition() {
805
695
  storeResolvedPosition(currentDetentIndex)
806
696
  }
807
697
 
808
- /**
809
- * Returns the estimated Y position for a detent index, using stored positions when available.
810
- */
811
698
  private fun getEstimatedPositionForIndex(index: Int): Int {
812
699
  if (index < 0 || index >= resolvedDetentPositions.size) return screenHeight
813
700
 
814
701
  val storedPos = resolvedDetentPositions[index]
815
702
  if (storedPos > 0) return storedPos
816
703
 
817
- // Estimate based on getDetentHeight which accounts for bottomInset and maxAllowedHeight
818
704
  if (index < detents.size) {
819
705
  val detentHeight = getDetentHeight(detents[index])
820
706
  return screenHeight - detentHeight
@@ -823,11 +709,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
823
709
  return screenHeight
824
710
  }
825
711
 
826
- /**
827
- * Finds the segment index and interpolation progress for a given position.
828
- * Returns a Triple of (fromIndex, toIndex, progress) where progress is 0-1 within that segment.
829
- * Returns null if position count is less than 2.
830
- */
712
+ /** Returns (fromIndex, toIndex, progress) for interpolation, or null if < 2 detents. */
831
713
  private fun findSegmentForPosition(positionPx: Int): Triple<Int, Int, Float>? {
832
714
  val count = resolvedDetentPositions.size
833
715
  if (count < 2) return null
@@ -835,19 +717,16 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
835
717
  val firstPos = getEstimatedPositionForIndex(0)
836
718
  val lastPos = getEstimatedPositionForIndex(count - 1)
837
719
 
838
- // Below first detent
839
720
  if (positionPx > firstPos) {
840
721
  val range = screenHeight - firstPos
841
722
  val progress = if (range > 0) (positionPx - firstPos).toFloat() / range else 0f
842
- return Triple(-1, 0, progress) // Special index -1 for below first
723
+ return Triple(-1, 0, progress)
843
724
  }
844
725
 
845
- // Above last detent
846
726
  if (positionPx < lastPos) {
847
727
  return Triple(count - 1, count - 1, 0f)
848
728
  }
849
729
 
850
- // Find segment (positions decrease as index increases)
851
730
  for (i in 0 until count - 1) {
852
731
  val pos = getEstimatedPositionForIndex(i)
853
732
  val nextPos = getEstimatedPositionForIndex(i + 1)
@@ -862,10 +741,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
862
741
  return Triple(count - 1, count - 1, 0f)
863
742
  }
864
743
 
865
- /**
866
- * Calculates the interpolated index based on position.
867
- * Returns a continuous value (e.g., 0.5 means halfway between detent 0 and 1).
868
- */
744
+ /** Returns continuous index (e.g., 0.5 = halfway between detent 0 and 1). */
869
745
  private fun getInterpolatedIndexForPosition(positionPx: Int): Float {
870
746
  val count = resolvedDetentPositions.size
871
747
  if (count == 0) return -1f
@@ -874,16 +750,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
874
750
  val segment = findSegmentForPosition(positionPx) ?: return 0f
875
751
  val (fromIndex, _, progress) = segment
876
752
 
877
- // Below first detent
878
753
  if (fromIndex == -1) return -progress
879
-
880
754
  return fromIndex + progress
881
755
  }
882
756
 
883
- /**
884
- * Calculates the interpolated detent value based on position.
885
- * Returns the actual screen fraction, clamped to valid detent range.
886
- */
757
+ /** Returns interpolated screen fraction for position. */
887
758
  private fun getInterpolatedDetentForPosition(positionPx: Int): Float {
888
759
  val count = resolvedDetentPositions.size
889
760
  if (count == 0) return 0f
@@ -891,7 +762,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
891
762
  val segment = findSegmentForPosition(positionPx) ?: return getDetentValueForIndex(0)
892
763
  val (fromIndex, toIndex, progress) = segment
893
764
 
894
- // Below first detent
895
765
  if (fromIndex == -1) {
896
766
  val firstDetent = getDetentValueForIndex(0)
897
767
  return maxOf(0f, firstDetent * (1 - progress))
@@ -902,11 +772,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
902
772
  return fromDetent + progress * (toDetent - fromDetent)
903
773
  }
904
774
 
905
- /**
906
- * Gets the detent value (fraction) for a given index.
907
- * Returns the raw screen fraction without bottomInset for interpolation calculations.
908
- * Note: bottomInset is only added in getDetentHeight() for actual sheet sizing.
909
- */
775
+ /** Returns raw screen fraction for index (without bottomInset). */
910
776
  private fun getDetentValueForIndex(index: Int): Float {
911
777
  if (index < 0 || index >= detents.size) return 0f
912
778
  val value = detents[index]
@@ -917,10 +783,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
917
783
  }
918
784
  }
919
785
 
920
- // ====================================================================
921
- // MARK: - Drag Handling
922
- // ====================================================================
923
-
924
786
  private fun getCurrentDetentInfo(sheetView: View): DetentInfo {
925
787
  val screenY = ScreenUtils.getScreenY(sheetView)
926
788
  return DetentInfo(currentDetentIndex, screenY.pxToDp())
@@ -948,14 +810,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
948
810
 
949
811
  private fun getDetentHeight(detent: Double): Int {
950
812
  val height: Int = if (detent == -1.0) {
951
- // For auto detent, add bottomInset to match iOS behavior where the system
952
- // adds bottom safe area inset internally to the sheet height
953
813
  contentHeight + headerHeight + bottomInset
954
814
  } else {
955
815
  if (detent <= 0.0 || detent > 1.0) {
956
816
  throw IllegalArgumentException("TrueSheet: detent fraction ($detent) must be between 0 and 1")
957
817
  }
958
- // For fractional detents, add bottomInset to match iOS behavior
959
818
  (detent * screenHeight).toInt() + bottomInset
960
819
  }
961
820
 
@@ -1035,8 +894,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1035
894
  super.onSizeChanged(w, h, oldw, oldh)
1036
895
  if (w == oldw && h == oldh) return
1037
896
 
1038
- // Skip when fully expanding to full screen (edgeToEdgeFullScreen enabled)
1039
- // Size keeps changing on this case
897
+ // Skip continuous size changes when fullScreen + edge-to-edge
1040
898
  if (h + statusBarHeight > screenHeight && isExpanded && oldw == w) {
1041
899
  return
1042
900
  }
@@ -1063,11 +921,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1063
921
  // MARK: - Touch Event Handling
1064
922
  // ====================================================================
1065
923
 
1066
- /**
1067
- * Custom touch dispatch to handle footer touch events.
1068
- * The footer is positioned outside the normal view hierarchy, so we need to
1069
- * manually check if touches fall within its bounds and forward them.
1070
- */
924
+ /** Forwards touch events to footer which is positioned outside normal hierarchy. */
1071
925
  override fun dispatchTouchEvent(event: MotionEvent): Boolean {
1072
926
  val footer = containerView?.footerView
1073
927
  if (footer != null && footer.isVisible) {
@@ -25,26 +25,18 @@
25
25
  NSInteger _pendingDetentIndex;
26
26
  BOOL _pendingContentSizeChange;
27
27
 
28
- // Position tracking
29
28
  CADisplayLink *_transitioningTimer;
30
29
  UIView *_transitionFakeView;
31
30
  BOOL _isDragging;
32
31
  BOOL _isTransitioning;
33
32
  BOOL _isTrackingPositionFromLayout;
34
33
 
35
- // Reference to parent TrueSheetViewController (if presented from another sheet)
36
34
  __weak TrueSheetViewController *_parentSheetController;
37
35
 
38
- // Blur effect view
39
36
  TrueSheetBlurView *_blurView;
40
-
41
- // Custom grabber view
42
37
  TrueSheetGrabberView *_grabberView;
43
38
 
44
- // Resolved detent positions (Y coordinate when sheet rests at each detent)
45
39
  NSMutableArray<NSNumber *> *_resolvedDetentPositions;
46
-
47
- // Tracks whether this sheet has a presented controller (e.g., RN Screens modal)
48
40
  BOOL _hasPresentedController;
49
41
  }
50
42
 
@@ -138,7 +130,6 @@
138
130
  [super viewDidLoad];
139
131
  self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
140
132
 
141
- // Create custom grabber view (hidden by default, shown when grabberOptions is set)
142
133
  _grabberView = [[TrueSheetGrabberView alloc] init];
143
134
  _grabberView.hidden = YES;
144
135
  [_grabberView addToView:self.view];
@@ -147,18 +138,14 @@
147
138
  - (void)viewWillAppear:(BOOL)animated {
148
139
  [super viewWillAppear:animated];
149
140
 
150
- // Only trigger on initial presentation, not repositioning
151
141
  if (!_isPresented) {
152
- // Initially store resolved position during presentation
153
142
  dispatch_async(dispatch_get_main_queue(), ^{
154
143
  [self storeResolvedPositionForIndex:self.currentDetentIndex];
155
144
  });
156
145
 
157
- // Capture parent sheet reference if presented from another TrueSheet
158
146
  UIViewController *presenter = self.presentingViewController;
159
147
  if ([presenter isKindOfClass:[TrueSheetViewController class]]) {
160
148
  _parentSheetController = (TrueSheetViewController *)presenter;
161
- // Notify parent that it is about to lose focus
162
149
  if ([_parentSheetController.delegate respondsToSelector:@selector(viewControllerWillBlur)]) {
163
150
  [_parentSheetController.delegate viewControllerWillBlur];
164
151
  }
@@ -173,7 +160,6 @@
173
160
  [self.delegate viewControllerWillPresentAtIndex:index position:position detent:detent];
174
161
  }
175
162
 
176
- // Emit willFocus with willPresent
177
163
  if ([self.delegate respondsToSelector:@selector(viewControllerWillFocus)]) {
178
164
  [self.delegate viewControllerWillFocus];
179
165
  }
@@ -187,7 +173,6 @@
187
173
  [super viewDidAppear:animated];
188
174
 
189
175
  if (!_isPresented) {
190
- // Notify parent that it has lost focus (after the child sheet appeared)
191
176
  if (_parentSheetController) {
192
177
  if ([_parentSheetController.delegate respondsToSelector:@selector(viewControllerDidBlur)]) {
193
178
  [_parentSheetController.delegate viewControllerDidBlur];
@@ -201,12 +186,10 @@
201
186
  [self.delegate viewControllerDidPresentAtIndex:index position:self.currentPosition detent:detent];
202
187
  }
203
188
 
204
- // Emit didFocus with didPresent
205
189
  if ([self.delegate respondsToSelector:@selector(viewControllerDidFocus)]) {
206
190
  [self.delegate viewControllerDidFocus];
207
191
  }
208
192
 
209
- // Emit correct position after presentation
210
193
  [self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:@"did present"];
211
194
  });
212
195
 
@@ -224,7 +207,6 @@
224
207
 
225
208
  if (self.isDismissing) {
226
209
  dispatch_async(dispatch_get_main_queue(), ^{
227
- // Emit willBlur with willDismiss
228
210
  if ([self.delegate respondsToSelector:@selector(viewControllerWillBlur)]) {
229
211
  [self.delegate viewControllerWillBlur];
230
212
  }
@@ -234,7 +216,6 @@
234
216
  }
235
217
  });
236
218
 
237
- // Notify the parent sheet (if any) that it is about to regain focus
238
219
  if (_parentSheetController) {
239
220
  if ([_parentSheetController.delegate respondsToSelector:@selector(viewControllerWillFocus)]) {
240
221
  [_parentSheetController.delegate viewControllerWillFocus];
@@ -248,12 +229,10 @@
248
229
  - (void)viewDidDisappear:(BOOL)animated {
249
230
  [super viewDidDisappear:animated];
250
231
 
251
- // Only dispatch didDismiss when actually dismissing (not when another modal is presented on top)
252
232
  if (self.isDismissing) {
253
233
  _isPresented = NO;
254
234
  _activeDetentIndex = -1;
255
235
 
256
- // Notify the parent sheet (if any) that it regained focus
257
236
  if (_parentSheetController) {
258
237
  if ([_parentSheetController.delegate respondsToSelector:@selector(viewControllerDidFocus)]) {
259
238
  [_parentSheetController.delegate viewControllerDidFocus];
@@ -261,7 +240,6 @@
261
240
  _parentSheetController = nil;
262
241
  }
263
242
 
264
- // Emit didBlur with didDismiss
265
243
  if ([self.delegate respondsToSelector:@selector(viewControllerDidBlur)]) {
266
244
  [self.delegate viewControllerDidBlur];
267
245
  }
@@ -279,17 +257,12 @@
279
257
  _isTrackingPositionFromLayout = YES;
280
258
 
281
259
  UIViewController *presented = self.presentedViewController;
282
-
283
- // Not realtime when another controller is presented that triggers our layout
284
260
  BOOL hasPresentedController = presented != nil && !presented.isBeingPresented && !presented.isBeingDismissed;
285
-
286
261
  BOOL realtime = !hasPresentedController;
287
262
 
288
263
  if (_pendingContentSizeChange) {
289
264
  _pendingContentSizeChange = NO;
290
265
  realtime = NO;
291
-
292
- // Store resolved position after content size changes
293
266
  [self storeResolvedPositionForIndex:self.currentDetentIndex];
294
267
  }
295
268
 
@@ -304,7 +277,6 @@
304
277
  [self.delegate viewControllerDidChangeSize:self.view.frame.size];
305
278
  }
306
279
 
307
- // Emit pending detent change after programmatic resize settles
308
280
  if (_pendingDetentIndex >= 0) {
309
281
  NSInteger pendingIndex = _pendingDetentIndex;
310
282
  _pendingDetentIndex = -1;
@@ -313,8 +285,6 @@
313
285
  if ([self.delegate respondsToSelector:@selector(viewControllerDidChangeDetent:position:detent:)]) {
314
286
  CGFloat detent = [self detentValueForIndex:pendingIndex];
315
287
  [self.delegate viewControllerDidChangeDetent:pendingIndex position:self.currentPosition detent:detent];
316
-
317
- // Emit position for the final position
318
288
  [self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:@"pending detent change"];
319
289
  }
320
290
  });
@@ -323,18 +293,15 @@
323
293
  _isTrackingPositionFromLayout = NO;
324
294
  }
325
295
 
326
- #pragma mark - Presentation Tracking (for RN Screens integration)
296
+ #pragma mark - Presentation Tracking (RN Screens)
327
297
 
328
298
  - (void)presentViewController:(UIViewController *)viewControllerToPresent
329
299
  animated:(BOOL)flag
330
300
  completion:(void (^)(void))completion {
331
- // Check if this is a non-TrueSheet controller (e.g., RN Screens modal)
332
301
  BOOL isExternalController = ![viewControllerToPresent isKindOfClass:[TrueSheetViewController class]];
333
302
 
334
303
  if (isExternalController && !_hasPresentedController) {
335
304
  _hasPresentedController = YES;
336
-
337
- // Emit blur events when an external controller is presented on top
338
305
  if ([self.delegate respondsToSelector:@selector(viewControllerWillBlur)]) {
339
306
  [self.delegate viewControllerWillBlur];
340
307
  }
@@ -359,7 +326,6 @@
359
326
  BOOL isExternalController = presented && ![presented isKindOfClass:[TrueSheetViewController class]];
360
327
 
361
328
  if (isExternalController && _hasPresentedController) {
362
- // Emit focus events when external controller is dismissed
363
329
  if ([self.delegate respondsToSelector:@selector(viewControllerWillFocus)]) {
364
330
  [self.delegate viewControllerWillFocus];
365
331
  }
@@ -379,7 +345,7 @@
379
345
  }];
380
346
  }
381
347
 
382
- #pragma mark - Gesture Handling
348
+ #pragma mark - Position & Gesture Handling
383
349
 
384
350
  - (TrueSheetContentView *)findContentView:(UIView *)view {
385
351
  if ([view isKindOfClass:[TrueSheetContentView class]]) {
@@ -401,11 +367,9 @@
401
367
  if (!presentedView)
402
368
  return;
403
369
 
404
- // Disable pan gestures if draggable is NO
405
370
  if (!self.draggable) {
406
371
  [GestureUtil setPanGesturesEnabled:NO forView:presentedView];
407
372
 
408
- // Also disable ScrollView's pan gesture if present
409
373
  TrueSheetContentView *contentView = [self findContentView:presentedView];
410
374
  if (contentView) {
411
375
  RCTScrollViewComponentView *scrollViewComponent = [contentView findScrollView:nil];
@@ -416,10 +380,8 @@
416
380
  return;
417
381
  }
418
382
 
419
- // Attach to presented view's pan gesture (sheet's drag gesture from UIKit)
420
383
  [GestureUtil attachPanGestureHandler:presentedView target:self selector:@selector(handlePanGesture:)];
421
384
 
422
- // Also attach to ScrollView's pan gesture if present
423
385
  TrueSheetContentView *contentView = [self findContentView:presentedView];
424
386
  if (contentView) {
425
387
  RCTScrollViewComponentView *scrollViewComponent = [contentView findScrollView:nil];
@@ -438,7 +400,6 @@
438
400
 
439
401
  [GestureUtil setPanGesturesEnabled:self.draggable forView:presentedView];
440
402
 
441
- // Also update ScrollView's pan gesture if present
442
403
  TrueSheetContentView *contentView = [self findContentView:presentedView];
443
404
  if (contentView) {
444
405
  RCTScrollViewComponentView *scrollViewComponent = [contentView findScrollView:nil];
@@ -469,10 +430,7 @@
469
430
  case UIGestureRecognizerStateCancelled: {
470
431
  if (!_isTransitioning) {
471
432
  dispatch_async(dispatch_get_main_queue(), ^{
472
- // Store resolved position when drag ends
473
433
  [self storeResolvedPositionForIndex:self.currentDetentIndex];
474
-
475
- // Emit the correct position after dragging
476
434
  [self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:@"drag end"];
477
435
  });
478
436
  }
@@ -485,8 +443,6 @@
485
443
  }
486
444
  }
487
445
 
488
- #pragma mark - Position Tracking
489
-
490
446
  - (void)setupTransitionTracker {
491
447
  if (!self.transitionCoordinator)
492
448
  return;
@@ -496,14 +452,11 @@
496
452
  CGRect dismissedFrame = CGRectMake(0, self.screenHeight, 0, 0);
497
453
  CGRect presentedFrame = CGRectMake(0, self.currentPosition, 0, 0);
498
454
 
499
- // Set starting fake view position
500
455
  _transitionFakeView.frame = self.isDismissing ? presentedFrame : dismissedFrame;
501
456
  [self storeResolvedPositionForIndex:self.currentDetentIndex];
502
457
 
503
458
  auto animation = ^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
504
459
  [[context containerView] addSubview:self->_transitionFakeView];
505
-
506
- // Set ending fake view position
507
460
  self->_transitionFakeView.frame = self.isDismissing ? dismissedFrame : presentedFrame;
508
461
 
509
462
  self->_transitioningTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleTransitionTracker)];
@@ -523,15 +476,12 @@
523
476
  - (void)handleTransitionTracker {
524
477
  if (!_isDragging && _transitionFakeView.layer) {
525
478
  CALayer *layer = _transitionFakeView.layer;
526
-
527
479
  CGFloat layerPosition = layer.presentationLayer.frame.origin.y;
528
480
 
529
481
  if (self.currentPosition >= self.screenHeight) {
530
- // Dismissing position
531
482
  CGFloat position = fmax(_lastPosition, layerPosition);
532
483
  [self emitChangePositionDelegateWithPosition:position realtime:YES debug:@"transition out"];
533
484
  } else {
534
- // Presenting position
535
485
  CGFloat position = fmax(self.currentPosition, layerPosition);
536
486
  [self emitChangePositionDelegateWithPosition:position realtime:YES debug:@"transition in"];
537
487
  }
@@ -539,7 +489,6 @@
539
489
  }
540
490
 
541
491
  - (void)emitChangePositionDelegateWithPosition:(CGFloat)position realtime:(BOOL)realtime debug:(NSString *)debug {
542
- // Use epsilon comparison to avoid missing updates due to floating point precision
543
492
  if (fabs(_lastPosition - position) > 0.01) {
544
493
  _lastPosition = position;
545
494
 
@@ -548,20 +497,15 @@
548
497
  if ([self.delegate respondsToSelector:@selector(viewControllerDidChangePosition:position:detent:realtime:)]) {
549
498
  [self.delegate viewControllerDidChangePosition:index position:position detent:detent realtime:realtime];
550
499
  }
551
-
552
- // Debug position tracking
553
- // NSLog(@"position from %@: %f, realtime: %i", debug, position, realtime);
554
500
  }
555
501
  }
556
502
 
557
- /// Stores the current position for the given detent index
558
503
  - (void)storeResolvedPositionForIndex:(NSInteger)index {
559
504
  if (index >= 0 && index < (NSInteger)_resolvedDetentPositions.count) {
560
505
  _resolvedDetentPositions[index] = @(self.currentPosition);
561
506
  }
562
507
  }
563
508
 
564
- /// Returns the estimated Y position for a detent index, using stored positions when available
565
509
  - (CGFloat)estimatedPositionForIndex:(NSInteger)index {
566
510
  if (index < 0 || index >= (NSInteger)_resolvedDetentPositions.count)
567
511
  return 0;
@@ -571,11 +515,9 @@
571
515
  return storedPos;
572
516
  }
573
517
 
574
- // Estimate based on detent value and known offset from first resolved position
575
518
  CGFloat detentValue = [self detentValueForIndex:index];
576
519
  CGFloat basePosition = self.screenHeight - (detentValue * self.screenHeight);
577
520
 
578
- // Find a resolved position to calculate offset
579
521
  for (NSInteger i = 0; i < (NSInteger)_resolvedDetentPositions.count; i++) {
580
522
  CGFloat pos = [_resolvedDetentPositions[i] doubleValue];
581
523
  if (pos > 0) {
@@ -589,8 +531,6 @@
589
531
  return basePosition;
590
532
  }
591
533
 
592
- /// Finds the segment containing the given position and returns the lower index and progress within that segment.
593
- /// Returns YES if a segment was found, NO otherwise. When NO, `outIndex` contains the boundary index.
594
534
  - (BOOL)findSegmentForPosition:(CGFloat)position outIndex:(NSInteger *)outIndex outProgress:(CGFloat *)outProgress {
595
535
  NSInteger count = _resolvedDetentPositions.count;
596
536
  if (count == 0) {
@@ -608,7 +548,6 @@
608
548
  CGFloat firstPos = [self estimatedPositionForIndex:0];
609
549
  CGFloat lastPos = [self estimatedPositionForIndex:count - 1];
610
550
 
611
- // Below first detent (position > firstPos means sheet is smaller)
612
551
  if (position > firstPos) {
613
552
  CGFloat range = self.screenHeight - firstPos;
614
553
  *outIndex = -1;
@@ -616,14 +555,12 @@
616
555
  return NO;
617
556
  }
618
557
 
619
- // Above last detent
620
558
  if (position < lastPos) {
621
559
  *outIndex = count - 1;
622
560
  *outProgress = 0;
623
561
  return NO;
624
562
  }
625
563
 
626
- // Find segment (positions decrease as index increases)
627
564
  for (NSInteger i = 0; i < count - 1; i++) {
628
565
  CGFloat pos = [self estimatedPositionForIndex:i];
629
566
  CGFloat nextPos = [self estimatedPositionForIndex:i + 1];
@@ -648,14 +585,11 @@
648
585
 
649
586
  if (!found) {
650
587
  if (index == -1) {
651
- // Below first detent - return negative progress
652
588
  return -progress;
653
589
  }
654
- // At or beyond boundary
655
590
  return index;
656
591
  }
657
592
 
658
- // Within a segment - interpolate
659
593
  return index + fmax(0, fmin(1, progress));
660
594
  }
661
595
 
@@ -666,15 +600,12 @@
666
600
 
667
601
  if (!found) {
668
602
  if (index == -1) {
669
- // Below first detent
670
603
  CGFloat firstDetent = [self detentValueForIndex:0];
671
604
  return fmax(0, firstDetent * (1 - progress));
672
605
  }
673
- // At or beyond boundary
674
606
  return [self detentValueForIndex:index];
675
607
  }
676
608
 
677
- // Within a segment - interpolate between detent values
678
609
  CGFloat detent = [self detentValueForIndex:index];
679
610
  CGFloat nextDetent = [self detentValueForIndex:index + 1];
680
611
  return detent + progress * (nextDetent - detent);
@@ -683,7 +614,6 @@
683
614
  - (CGFloat)detentValueForIndex:(NSInteger)index {
684
615
  if (index >= 0 && index < (NSInteger)_detents.count) {
685
616
  CGFloat value = [_detents[index] doubleValue];
686
- // For auto (-1), calculate actual fraction from content + header height
687
617
  if (value == -1) {
688
618
  CGFloat autoHeight = [self.contentHeight floatValue] + [self.headerHeight floatValue];
689
619
  return autoHeight / self.screenHeight;
@@ -716,13 +646,11 @@
716
646
  withAutoHeight:autoHeight
717
647
  atIndex:index];
718
648
  [detents addObject:sheetDetent];
719
- // Initialize with placeholder - will be updated when sheet settles at each detent
720
649
  [_resolvedDetentPositions addObject:@(0)];
721
650
  }
722
651
 
723
652
  sheet.detents = detents;
724
653
 
725
- // Setup dimmed background
726
654
  if (self.dimmed && [self.dimmedDetentIndex integerValue] == 0) {
727
655
  sheet.largestUndimmedDetentIdentifier = nil;
728
656
  } else {
@@ -752,7 +680,6 @@
752
680
 
753
681
  CGFloat value = [detent doubleValue];
754
682
 
755
- // -1 represents "auto" (fit content height)
756
683
  if (value == -1) {
757
684
  if (@available(iOS 16.0, *)) {
758
685
  return [self customDetentWithIdentifier:@"custom-auto" height:autoHeight];
@@ -818,7 +745,6 @@
818
745
  if (detentCount == 0)
819
746
  return;
820
747
 
821
- // Clamp index to valid range
822
748
  NSInteger clampedIndex = _activeDetentIndex;
823
749
  if (clampedIndex < 0) {
824
750
  clampedIndex = 0;
@@ -875,7 +801,6 @@
875
801
 
876
802
  self.view.backgroundColor = self.backgroundColor;
877
803
 
878
- // Setup blur effect view - recreate only when blurTint changes
879
804
  BOOL blurTintChanged = ![_blurView.blurTint isEqualToString:self.blurTint];
880
805
 
881
806
  if (_blurView && blurTintChanged) {
@@ -894,11 +819,9 @@
894
819
  [_blurView applyBlurEffect];
895
820
  }
896
821
 
897
- // Setup grabber
898
822
  BOOL showGrabber = self.grabber && self.draggable;
899
823
 
900
824
  if (self.grabberOptions) {
901
- // Use custom grabber view when options are provided
902
825
  sheet.prefersGrabberVisible = NO;
903
826
 
904
827
  NSDictionary *options = self.grabberOptions;
@@ -910,7 +833,6 @@
910
833
  [_grabberView applyConfiguration];
911
834
  _grabberView.hidden = !showGrabber;
912
835
  } else {
913
- // Use system default grabber when no options provided
914
836
  sheet.prefersGrabberVisible = showGrabber;
915
837
  _grabberView.hidden = YES;
916
838
  }
@@ -935,13 +857,10 @@
935
857
 
936
858
  #if RNS_DISMISSIBLE_MODAL_PROTOCOL_AVAILABLE
937
859
  - (BOOL)isDismissible {
938
- // Prevent react-native-screens from dismissing this sheet when presenting a modal
939
860
  return NO;
940
861
  }
941
862
 
942
863
  - (UIViewController *)newPresentingViewController {
943
- // Find the topmost TrueSheetViewController in the chain
944
- // This handles cases where this sheet is presenting another sheet (child sheet)
945
864
  UIViewController *topmost = self;
946
865
  while (topmost.presentedViewController != nil && !topmost.presentedViewController.isBeingDismissed &&
947
866
  [topmost.presentedViewController isKindOfClass:[TrueSheetViewController class]]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lodev09/react-native-true-sheet",
3
- "version": "3.1.0-beta.7",
3
+ "version": "3.1.0-beta.8",
4
4
  "description": "The true native bottom sheet experience for your React Native Apps.",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./lib/module/index.js",