@lodev09/react-native-true-sheet 3.0.0-beta.11 → 3.0.0-beta.13

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 (61) hide show
  1. package/README.md +1 -1
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +3 -1
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +33 -45
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +151 -22
  5. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDialogObserver.kt +93 -0
  6. package/android/src/main/java/com/lodev09/truesheet/events/BlurEvent.kt +20 -0
  7. package/android/src/main/java/com/lodev09/truesheet/events/DetentChangeEvent.kt +2 -1
  8. package/android/src/main/java/com/lodev09/truesheet/events/DidPresentEvent.kt +2 -1
  9. package/android/src/main/java/com/lodev09/truesheet/events/DragBeginEvent.kt +2 -1
  10. package/android/src/main/java/com/lodev09/truesheet/events/DragChangeEvent.kt +2 -1
  11. package/android/src/main/java/com/lodev09/truesheet/events/DragEndEvent.kt +2 -1
  12. package/android/src/main/java/com/lodev09/truesheet/events/FocusEvent.kt +20 -0
  13. package/android/src/main/java/com/lodev09/truesheet/events/PositionChangeEvent.kt +5 -3
  14. package/android/src/main/java/com/lodev09/truesheet/events/WillPresentEvent.kt +2 -1
  15. package/android/src/main/res/anim/true_sheet_slide_in.xml +13 -0
  16. package/android/src/main/res/anim/true_sheet_slide_out.xml +13 -0
  17. package/android/src/main/res/values/styles.xml +13 -1
  18. package/ios/TrueSheetContentView.mm +37 -30
  19. package/ios/TrueSheetView.mm +26 -8
  20. package/ios/TrueSheetViewController.h +7 -1
  21. package/ios/TrueSheetViewController.mm +110 -40
  22. package/ios/events/OnDetentChangeEvent.h +2 -1
  23. package/ios/events/OnDetentChangeEvent.mm +3 -1
  24. package/ios/events/OnDidBlurEvent.h +26 -0
  25. package/ios/events/OnDidBlurEvent.mm +25 -0
  26. package/ios/events/OnDidFocusEvent.h +26 -0
  27. package/ios/events/OnDidFocusEvent.mm +25 -0
  28. package/ios/events/OnDidPresentEvent.h +2 -1
  29. package/ios/events/OnDidPresentEvent.mm +3 -1
  30. package/ios/events/OnDragBeginEvent.h +2 -1
  31. package/ios/events/OnDragBeginEvent.mm +3 -1
  32. package/ios/events/OnDragChangeEvent.h +2 -1
  33. package/ios/events/OnDragChangeEvent.mm +3 -1
  34. package/ios/events/OnDragEndEvent.h +2 -1
  35. package/ios/events/OnDragEndEvent.mm +3 -1
  36. package/ios/events/OnPositionChangeEvent.h +2 -1
  37. package/ios/events/OnPositionChangeEvent.mm +4 -2
  38. package/ios/events/OnWillPresentEvent.h +2 -1
  39. package/ios/events/OnWillPresentEvent.mm +3 -1
  40. package/lib/module/TrueSheet.js +10 -0
  41. package/lib/module/TrueSheet.js.map +1 -1
  42. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +5 -1
  43. package/lib/module/reanimated/ReanimatedTrueSheet.js +9 -4
  44. package/lib/module/reanimated/ReanimatedTrueSheet.js.map +1 -1
  45. package/lib/module/reanimated/ReanimatedTrueSheetProvider.js +4 -2
  46. package/lib/module/reanimated/ReanimatedTrueSheetProvider.js.map +1 -1
  47. package/lib/typescript/src/TrueSheet.d.ts +2 -0
  48. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  49. package/lib/typescript/src/TrueSheet.types.d.ts +14 -0
  50. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  51. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +5 -1
  52. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  53. package/lib/typescript/src/reanimated/ReanimatedTrueSheet.d.ts.map +1 -1
  54. package/lib/typescript/src/reanimated/ReanimatedTrueSheetProvider.d.ts +8 -2
  55. package/lib/typescript/src/reanimated/ReanimatedTrueSheetProvider.d.ts.map +1 -1
  56. package/package.json +1 -1
  57. package/src/TrueSheet.tsx +14 -0
  58. package/src/TrueSheet.types.ts +16 -0
  59. package/src/fabric/TrueSheetViewNativeComponent.ts +5 -1
  60. package/src/reanimated/ReanimatedTrueSheet.tsx +9 -4
  61. package/src/reanimated/ReanimatedTrueSheetProvider.tsx +11 -3
@@ -8,7 +8,7 @@ import com.facebook.react.uimanager.events.Event
8
8
  * Fired continuously while user is dragging the sheet
9
9
  * Payload: { index: number, position: number }
10
10
  */
11
- class DragChangeEvent(surfaceId: Int, viewId: Int, private val index: Int, private val position: Float) :
11
+ class DragChangeEvent(surfaceId: Int, viewId: Int, private val index: Int, private val position: Float, private val detent: Float) :
12
12
  Event<DragChangeEvent>(surfaceId, viewId) {
13
13
 
14
14
  override fun getEventName(): String = EVENT_NAME
@@ -17,6 +17,7 @@ class DragChangeEvent(surfaceId: Int, viewId: Int, private val index: Int, priva
17
17
  Arguments.createMap().apply {
18
18
  putInt("index", index)
19
19
  putDouble("position", position.toDouble())
20
+ putDouble("detent", detent.toDouble())
20
21
  }
21
22
 
22
23
  companion object {
@@ -8,7 +8,7 @@ import com.facebook.react.uimanager.events.Event
8
8
  * Fired when user stops dragging the sheet
9
9
  * Payload: { index: number, position: number }
10
10
  */
11
- class DragEndEvent(surfaceId: Int, viewId: Int, private val index: Int, private val position: Float) :
11
+ class DragEndEvent(surfaceId: Int, viewId: Int, private val index: Int, private val position: Float, private val detent: Float) :
12
12
  Event<DragEndEvent>(surfaceId, viewId) {
13
13
 
14
14
  override fun getEventName(): String = EVENT_NAME
@@ -17,6 +17,7 @@ class DragEndEvent(surfaceId: Int, viewId: Int, private val index: Int, private
17
17
  Arguments.createMap().apply {
18
18
  putInt("index", index)
19
19
  putDouble("position", position.toDouble())
20
+ putDouble("detent", detent.toDouble())
20
21
  }
21
22
 
22
23
  companion object {
@@ -0,0 +1,20 @@
1
+ package com.lodev09.truesheet.events
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+ import com.facebook.react.uimanager.events.Event
6
+
7
+ /**
8
+ * Fired when the sheet regains focus after a sheet on top of it is dismissed
9
+ */
10
+ class FocusEvent(surfaceId: Int, viewId: Int) : Event<FocusEvent>(surfaceId, viewId) {
11
+
12
+ override fun getEventName(): String = EVENT_NAME
13
+
14
+ override fun getEventData(): WritableMap = Arguments.createMap()
15
+
16
+ companion object {
17
+ const val EVENT_NAME = "topDidFocus"
18
+ const val REGISTRATION_NAME = "onDidFocus"
19
+ }
20
+ }
@@ -6,13 +6,14 @@ import com.facebook.react.uimanager.events.Event
6
6
 
7
7
  /**
8
8
  * Fired continuously for position updates during drag and animation
9
- * Payload: { index: number, position: number, transitioning: boolean }
9
+ * Payload: { index: number, position: number, detent: number, transitioning: boolean }
10
10
  */
11
11
  class PositionChangeEvent(
12
12
  surfaceId: Int,
13
13
  viewId: Int,
14
- private val index: Int,
14
+ private val index: Float,
15
15
  private val position: Float,
16
+ private val detent: Float,
16
17
  private val transitioning: Boolean = false
17
18
  ) : Event<PositionChangeEvent>(surfaceId, viewId) {
18
19
 
@@ -20,8 +21,9 @@ class PositionChangeEvent(
20
21
 
21
22
  override fun getEventData(): WritableMap =
22
23
  Arguments.createMap().apply {
23
- putInt("index", index)
24
+ putDouble("index", index.toDouble())
24
25
  putDouble("position", position.toDouble())
26
+ putDouble("detent", detent.toDouble())
25
27
  putBoolean("transitioning", transitioning)
26
28
  }
27
29
 
@@ -8,7 +8,7 @@ import com.facebook.react.uimanager.events.Event
8
8
  * Fired before the sheet is presented
9
9
  * Payload: { index: number, position: number }
10
10
  */
11
- class WillPresentEvent(surfaceId: Int, viewId: Int, private val index: Int, private val position: Float) :
11
+ class WillPresentEvent(surfaceId: Int, viewId: Int, private val index: Int, private val position: Float, private val detent: Float) :
12
12
  Event<WillPresentEvent>(surfaceId, viewId) {
13
13
 
14
14
  override fun getEventName(): String = EVENT_NAME
@@ -17,6 +17,7 @@ class WillPresentEvent(surfaceId: Int, viewId: Int, private val index: Int, priv
17
17
  Arguments.createMap().apply {
18
18
  putInt("index", index)
19
19
  putDouble("position", position.toDouble())
20
+ putDouble("detent", detent.toDouble())
20
21
  }
21
22
 
22
23
  companion object {
@@ -0,0 +1,13 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <set xmlns:android="http://schemas.android.com/apk/res/android">
3
+ <translate
4
+ android:duration="200"
5
+ android:fromYDelta="20%p"
6
+ android:toYDelta="0"
7
+ android:interpolator="@android:interpolator/fast_out_slow_in" />
8
+ <alpha
9
+ android:duration="200"
10
+ android:fromAlpha="0.0"
11
+ android:toAlpha="1.0"
12
+ android:interpolator="@android:interpolator/fast_out_slow_in" />
13
+ </set>
@@ -0,0 +1,13 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <set xmlns:android="http://schemas.android.com/apk/res/android">
3
+ <translate
4
+ android:duration="150"
5
+ android:fromYDelta="0"
6
+ android:toYDelta="20%p"
7
+ android:interpolator="@android:interpolator/fast_out_slow_in" />
8
+ <alpha
9
+ android:duration="150"
10
+ android:fromAlpha="1.0"
11
+ android:toAlpha="0.0"
12
+ android:interpolator="@android:interpolator/fast_out_slow_in" />
13
+ </set>
@@ -1,8 +1,20 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <resources>
3
- <!-- Custom BottomSheetDialog style with edge-to-edge enabled -->
3
+ <!-- Smooth slide animation for BottomSheetDialog -->
4
+ <style name="TrueSheetAnimation" parent="Animation.AppCompat.Dialog">
5
+ <item name="android:windowEnterAnimation">@anim/true_sheet_slide_in</item>
6
+ <item name="android:windowExitAnimation">@anim/true_sheet_slide_out</item>
7
+ </style>
8
+
9
+ <!-- Default BottomSheetDialog style with smooth animations -->
10
+ <style name="TrueSheetDialog" parent="Theme.Design.Light.BottomSheetDialog">
11
+ <item name="android:windowAnimationStyle">@style/TrueSheetAnimation</item>
12
+ </style>
13
+
14
+ <!-- BottomSheetDialog style with edge-to-edge and smooth animations -->
4
15
  <style name="TrueSheetEdgeToEdgeEnabledDialog" parent="Theme.Design.Light.BottomSheetDialog">
5
16
  <item name="android:windowIsFloating">false</item>
6
17
  <item name="enableEdgeToEdge">true</item>
18
+ <item name="android:windowAnimationStyle">@style/TrueSheetAnimation</item>
7
19
  </style>
8
20
  </resources>
@@ -126,46 +126,53 @@ using namespace facebook::react;
126
126
  }
127
127
  }
128
128
 
129
+ - (RCTScrollViewComponentView *)findScrollViewInSubviews:(NSArray<UIView *> *)subviews {
130
+ for (UIView *subview in subviews) {
131
+ if ([subview isKindOfClass:RCTScrollViewComponentView.class]) {
132
+ return (RCTScrollViewComponentView *)subview;
133
+ }
134
+ }
135
+ return nil;
136
+ }
137
+
129
138
  - (RCTScrollViewComponentView *)findScrollView:(UIView **)outTopSibling {
130
139
  if (self.subviews.count == 0) {
131
140
  return nil;
132
141
  }
133
142
 
134
- RCTScrollViewComponentView *scrollView = nil;
135
143
  UIView *topSibling = nil;
136
144
 
137
145
  // Check first-level children for scroll views (ScrollView or FlatList)
138
- for (UIView *subview in self.subviews) {
139
- if ([subview isKindOfClass:RCTScrollViewComponentView.class]) {
140
- scrollView = (RCTScrollViewComponentView *)subview;
141
-
142
- // Find the view positioned directly above this ScrollView by frame position
143
- if (self.subviews.count > 1) {
144
- CGFloat scrollViewTop = CGRectGetMinY(scrollView.frame);
145
- CGFloat closestDistance = CGFLOAT_MAX;
146
-
147
- for (UIView *sibling in self.subviews) {
148
- // Skip the ScrollView itself
149
- if (sibling == scrollView) {
150
- continue;
151
- }
152
-
153
- CGFloat siblingBottom = CGRectGetMaxY(sibling.frame);
154
-
155
- // Check if this sibling is positioned above the ScrollView
156
- if (siblingBottom <= scrollViewTop) {
157
- CGFloat distance = scrollViewTop - siblingBottom;
158
-
159
- // Find the closest view above (smallest distance)
160
- if (distance < closestDistance) {
161
- closestDistance = distance;
162
- topSibling = sibling;
163
- }
164
- }
165
- }
146
+ RCTScrollViewComponentView *scrollView = [self findScrollViewInSubviews:self.subviews];
147
+
148
+ // If not found, check second level (grandchildren)
149
+ if (!scrollView) {
150
+ for (UIView *subview in self.subviews) {
151
+ scrollView = [self findScrollViewInSubviews:subview.subviews];
152
+ if (scrollView) {
153
+ break;
166
154
  }
155
+ }
156
+ }
167
157
 
168
- break; // Found ScrollView, no need to continue
158
+ // Find the view positioned directly above the ScrollView (only for first-level)
159
+ if (scrollView && scrollView.superview == self && self.subviews.count > 1) {
160
+ CGFloat scrollViewTop = CGRectGetMinY(scrollView.frame);
161
+ CGFloat closestDistance = CGFLOAT_MAX;
162
+
163
+ for (UIView *sibling in self.subviews) {
164
+ if (sibling == scrollView) {
165
+ continue;
166
+ }
167
+
168
+ CGFloat siblingBottom = CGRectGetMaxY(sibling.frame);
169
+ if (siblingBottom <= scrollViewTop) {
170
+ CGFloat distance = scrollViewTop - siblingBottom;
171
+ if (distance < closestDistance) {
172
+ closestDistance = distance;
173
+ topSibling = sibling;
174
+ }
175
+ }
169
176
  }
170
177
  }
171
178
 
@@ -15,7 +15,9 @@
15
15
  #import "TrueSheetModule.h"
16
16
  #import "TrueSheetViewController.h"
17
17
  #import "events/OnDetentChangeEvent.h"
18
+ #import "events/OnDidBlurEvent.h"
18
19
  #import "events/OnDidDismissEvent.h"
20
+ #import "events/OnDidFocusEvent.h"
19
21
  #import "events/OnDidPresentEvent.h"
20
22
  #import "events/OnDragBeginEvent.h"
21
23
  #import "events/OnDragChangeEvent.h"
@@ -369,24 +371,28 @@ using namespace facebook::react;
369
371
  - (void)viewControllerWillPresent {
370
372
  NSInteger index = [_controller currentDetentIndex];
371
373
  _controller.activeDetentIndex = index;
372
- [OnWillPresentEvent emit:_eventEmitter index:index position:_controller.currentPosition];
374
+ CGFloat detent = [_controller detentValueForIndex:index];
375
+ [OnWillPresentEvent emit:_eventEmitter index:index position:_controller.currentPosition detent:detent];
373
376
  }
374
377
 
375
378
  - (void)viewControllerDidPresent {
376
- [OnDidPresentEvent emit:_eventEmitter index:[_controller currentDetentIndex] position:_controller.currentPosition];
379
+ NSInteger index = [_controller currentDetentIndex];
380
+ CGFloat detent = [_controller detentValueForIndex:index];
381
+ [OnDidPresentEvent emit:_eventEmitter index:index position:_controller.currentPosition detent:detent];
377
382
  }
378
383
 
379
384
  - (void)viewControllerDidDrag:(UIGestureRecognizerState)state index:(NSInteger)index position:(CGFloat)position {
385
+ CGFloat detent = [_controller detentValueForIndex:index];
380
386
  switch (state) {
381
387
  case UIGestureRecognizerStateBegan:
382
- [OnDragBeginEvent emit:_eventEmitter index:index position:position];
388
+ [OnDragBeginEvent emit:_eventEmitter index:index position:position detent:detent];
383
389
  break;
384
390
  case UIGestureRecognizerStateChanged:
385
- [OnDragChangeEvent emit:_eventEmitter index:index position:position];
391
+ [OnDragChangeEvent emit:_eventEmitter index:index position:position detent:detent];
386
392
  break;
387
393
  case UIGestureRecognizerStateEnded:
388
394
  case UIGestureRecognizerStateCancelled:
389
- [OnDragEndEvent emit:_eventEmitter index:index position:position];
395
+ [OnDragEndEvent emit:_eventEmitter index:index position:position detent:detent];
390
396
  break;
391
397
  default:
392
398
  break;
@@ -406,17 +412,29 @@ using namespace facebook::react;
406
412
  if (_controller.activeDetentIndex != index) {
407
413
  _controller.activeDetentIndex = index;
408
414
  }
409
- [OnDetentChangeEvent emit:_eventEmitter index:index position:position];
415
+ CGFloat detent = [_controller detentValueForIndex:index];
416
+ [OnDetentChangeEvent emit:_eventEmitter index:index position:position detent:detent];
410
417
  }
411
418
 
412
- - (void)viewControllerDidChangePosition:(NSInteger)index position:(CGFloat)position transitioning:(BOOL)transitioning {
413
- [OnPositionChangeEvent emit:_eventEmitter index:index position:position transitioning:transitioning];
419
+ - (void)viewControllerDidChangePosition:(CGFloat)index
420
+ position:(CGFloat)position
421
+ detent:(CGFloat)detent
422
+ transitioning:(BOOL)transitioning {
423
+ [OnPositionChangeEvent emit:_eventEmitter index:index position:position detent:detent transitioning:transitioning];
414
424
  }
415
425
 
416
426
  - (void)viewControllerDidChangeSize:(CGSize)size {
417
427
  [self updateStateWithSize:size];
418
428
  }
419
429
 
430
+ - (void)viewControllerDidFocus {
431
+ [OnDidFocusEvent emit:_eventEmitter];
432
+ }
433
+
434
+ - (void)viewControllerDidBlur {
435
+ [OnDidBlurEvent emit:_eventEmitter];
436
+ }
437
+
420
438
  #pragma mark - Private Helpers
421
439
 
422
440
  - (UIViewController *)findPresentingViewController {
@@ -25,8 +25,13 @@ NS_ASSUME_NONNULL_BEGIN
25
25
  - (void)viewControllerDidDismiss;
26
26
  - (void)viewControllerDidChangeDetent:(NSInteger)index position:(CGFloat)position;
27
27
  - (void)viewControllerDidDrag:(UIGestureRecognizerState)state index:(NSInteger)index position:(CGFloat)position;
28
- - (void)viewControllerDidChangePosition:(NSInteger)index position:(CGFloat)position transitioning:(BOOL)transitioning;
28
+ - (void)viewControllerDidChangePosition:(CGFloat)index
29
+ position:(CGFloat)position
30
+ detent:(CGFloat)detent
31
+ transitioning:(BOOL)transitioning;
29
32
  - (void)viewControllerDidChangeSize:(CGSize)size;
33
+ - (void)viewControllerDidFocus;
34
+ - (void)viewControllerDidBlur;
30
35
 
31
36
  @end
32
37
 
@@ -60,6 +65,7 @@ NS_ASSUME_NONNULL_BEGIN
60
65
  - (NSInteger)currentDetentIndex;
61
66
  - (CGFloat)currentPosition;
62
67
  - (CGFloat)bottomInset;
68
+ - (CGFloat)detentValueForIndex:(NSInteger)index;
63
69
 
64
70
  @end
65
71
 
@@ -29,6 +29,9 @@
29
29
  // Hidden view used to track position during native transition animations
30
30
  UIView *_fakeTransitionView;
31
31
  CADisplayLink *_displayLink;
32
+
33
+ // Reference to parent TrueSheetViewController (if presented from another sheet)
34
+ __weak TrueSheetViewController *_parentSheetController;
32
35
  }
33
36
 
34
37
  #pragma mark - Initialization
@@ -97,13 +100,8 @@
97
100
  return window ? window.safeAreaInsets.bottom : 0;
98
101
  }
99
102
 
100
- - (CGFloat)currentHeight {
101
- return self.containerHeight - self.currentPosition - self.bottomInset;
102
- }
103
-
104
- - (CGFloat)containerHeight {
105
- UIView *sheetContainerView = self.sheetPresentationController.containerView;
106
- return sheetContainerView ? sheetContainerView.frame.size.height : 0.0;
103
+ - (CGFloat)screenHeight {
104
+ return UIScreen.mainScreen.bounds.size.height;
107
105
  }
108
106
 
109
107
  - (NSInteger)currentDetentIndex {
@@ -146,6 +144,16 @@
146
144
 
147
145
  // Only trigger on initial presentation, not repositioning
148
146
  if (!_isPresented) {
147
+ // Capture parent sheet reference if presented from another TrueSheet
148
+ UIViewController *presenter = self.presentingViewController;
149
+ if ([presenter isKindOfClass:[TrueSheetViewController class]]) {
150
+ _parentSheetController = (TrueSheetViewController *)presenter;
151
+ // Notify parent that it lost focus
152
+ if ([_parentSheetController.delegate respondsToSelector:@selector(viewControllerDidBlur)]) {
153
+ [_parentSheetController.delegate viewControllerDidBlur];
154
+ }
155
+ }
156
+
149
157
  if ([self.delegate respondsToSelector:@selector(viewControllerWillPresent)]) {
150
158
  [self.delegate viewControllerWillPresent];
151
159
  }
@@ -182,8 +190,18 @@
182
190
  // Only dispatch didDismiss when actually dismissing (not when another modal is presented on top)
183
191
  BOOL isActuallyDismissing = self.presentingViewController == nil || self.isBeingDismissed;
184
192
 
185
- if (isActuallyDismissing && [self.delegate respondsToSelector:@selector(viewControllerDidDismiss)]) {
186
- [self.delegate viewControllerDidDismiss];
193
+ if (isActuallyDismissing) {
194
+ // Notify the parent sheet (if any) that it regained focus
195
+ if (_parentSheetController) {
196
+ if ([_parentSheetController.delegate respondsToSelector:@selector(viewControllerDidFocus)]) {
197
+ [_parentSheetController.delegate viewControllerDidFocus];
198
+ }
199
+ _parentSheetController = nil;
200
+ }
201
+
202
+ if ([self.delegate respondsToSelector:@selector(viewControllerDidDismiss)]) {
203
+ [self.delegate viewControllerDidDismiss];
204
+ }
187
205
  }
188
206
 
189
207
  _isTrackingPositionFromLayout = NO;
@@ -283,13 +301,67 @@
283
301
  if (_lastPosition != position) {
284
302
  _lastPosition = position;
285
303
 
286
- NSInteger index = [self currentDetentIndex];
287
- if ([self.delegate respondsToSelector:@selector(viewControllerDidChangePosition:position:transitioning:)]) {
288
- [self.delegate viewControllerDidChangePosition:index position:position transitioning:transitioning];
304
+ CGFloat index = [self interpolatedIndexForPosition:position];
305
+ NSInteger discreteIndex = (NSInteger)round(index);
306
+ CGFloat detent = [self detentValueForIndex:discreteIndex];
307
+ if ([self.delegate respondsToSelector:@selector(viewControllerDidChangePosition:position:detent:transitioning:)]) {
308
+ [self.delegate viewControllerDidChangePosition:index position:position detent:detent transitioning:transitioning];
289
309
  }
290
310
  }
291
311
  }
292
312
 
313
+ - (CGFloat)interpolatedIndexForPosition:(CGFloat)position {
314
+ NSInteger count = _detents.count;
315
+ if (count == 0)
316
+ return -1;
317
+ if (count == 1)
318
+ return 0;
319
+
320
+ // Convert position to detent fraction: detent = (screenHeight - position) / screenHeight
321
+ CGFloat currentDetent = (self.screenHeight - position) / self.screenHeight;
322
+
323
+ // Handle below first detent (interpolate from -1 to 0)
324
+ CGFloat firstDetentValue = [self detentValueForIndex:0];
325
+ if (currentDetent < firstDetentValue) {
326
+ // Interpolate: 0 at firstDetentValue, -1 at 0
327
+ if (firstDetentValue <= 0)
328
+ return 0;
329
+ CGFloat progress = currentDetent / firstDetentValue;
330
+ return progress - 1;
331
+ }
332
+
333
+ // Find which segment the current detent falls into and interpolate
334
+ for (NSInteger i = 0; i < count - 1; i++) {
335
+ CGFloat detentValue = [self detentValueForIndex:i];
336
+ CGFloat nextDetentValue = [self detentValueForIndex:i + 1];
337
+
338
+ if (currentDetent <= nextDetentValue) {
339
+ // Interpolate between index i and i+1
340
+ CGFloat range = nextDetentValue - detentValue;
341
+ if (range <= 0)
342
+ return i;
343
+ CGFloat progress = (currentDetent - detentValue) / range;
344
+ return i + fmax(0, fmin(1, progress));
345
+ }
346
+ }
347
+
348
+ return count - 1;
349
+ }
350
+
351
+ - (CGFloat)detentValueForIndex:(NSInteger)index {
352
+ if (index >= 0 && index < (NSInteger)_detents.count) {
353
+ CGFloat value = [_detents[index] floatValue];
354
+ // For auto (-1), calculate actual fraction from content + header height
355
+ // Subtract bottomInset to match the actual sheet height (consistent with detent resolver)
356
+ if (value == -1) {
357
+ CGFloat autoHeight = [self.contentHeight floatValue] + [self.headerHeight floatValue] - self.bottomInset;
358
+ return autoHeight / self.screenHeight;
359
+ }
360
+ return value;
361
+ }
362
+ return 0;
363
+ }
364
+
293
365
  /**
294
366
  * Sets up position tracking during view controller transitions using a fake view.
295
367
  *
@@ -313,7 +385,7 @@
313
385
  BOOL isPresenting = self.isBeingPresented;
314
386
 
315
387
  // Set initial position: presenting starts from bottom, dismissing from current
316
- frame.origin.y = isPresenting ? self.containerHeight : presentedView.frame.origin.y;
388
+ frame.origin.y = isPresenting ? self.screenHeight : presentedView.frame.origin.y;
317
389
  _fakeTransitionView.frame = frame;
318
390
 
319
391
  auto animation = ^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
@@ -375,12 +447,12 @@
375
447
  NSMutableArray<UISheetPresentationControllerDetent *> *detents = [NSMutableArray array];
376
448
 
377
449
  // Subtract bottomInset to prevent iOS from adding extra bottom insets
378
- CGFloat totalHeight = [self.contentHeight floatValue] + [self.headerHeight floatValue] - self.bottomInset;
450
+ CGFloat autoHeight = [self.contentHeight floatValue] + [self.headerHeight floatValue] - self.bottomInset;
379
451
 
380
452
  for (NSInteger index = 0; index < self.detents.count; index++) {
381
453
  id detent = self.detents[index];
382
454
  UISheetPresentationControllerDetent *sheetDetent = [self detentForValue:detent
383
- withHeight:totalHeight
455
+ withAutoHeight:autoHeight
384
456
  atIndex:index];
385
457
  [detents addObject:sheetDetent];
386
458
  }
@@ -408,7 +480,9 @@
408
480
  }
409
481
  }
410
482
 
411
- - (UISheetPresentationControllerDetent *)detentForValue:(id)detent withHeight:(CGFloat)height atIndex:(NSInteger)index {
483
+ - (UISheetPresentationControllerDetent *)detentForValue:(id)detent
484
+ withAutoHeight:(CGFloat)autoHeight
485
+ atIndex:(NSInteger)index {
412
486
  if (![detent isKindOfClass:[NSNumber class]]) {
413
487
  return [UISheetPresentationControllerDetent mediumDetent];
414
488
  }
@@ -418,15 +492,9 @@
418
492
  // -1 represents "auto" (fit content height)
419
493
  if (value == -1) {
420
494
  if (@available(iOS 16.0, *)) {
421
- NSString *detentId = @"custom-auto";
422
- return [UISheetPresentationControllerDetent
423
- customDetentWithIdentifier:detentId
424
- resolver:^CGFloat(id<UISheetPresentationControllerDetentResolutionContext> context) {
425
- CGFloat maxDetentValue = context.maximumDetentValue;
426
- CGFloat maxValue =
427
- self.maxHeight ? fmin(maxDetentValue, [self.maxHeight floatValue]) : maxDetentValue;
428
- return fmin(height, maxValue);
429
- }];
495
+ // Subtract bottomInset so position matches screenHeight - sheetHeight
496
+ // iOS adds bottom safe area to sheet height internally
497
+ return [self customDetentWithIdentifier:@"custom-auto" height:autoHeight - self.bottomInset];
430
498
  } else {
431
499
  return [UISheetPresentationControllerDetent mediumDetent];
432
500
  }
@@ -438,21 +506,11 @@
438
506
  }
439
507
 
440
508
  if (@available(iOS 16.0, *)) {
441
- if (value == 1.0) {
442
- return [UISheetPresentationControllerDetent largeDetent];
443
- } else if (value == 0.5) {
444
- return [UISheetPresentationControllerDetent mediumDetent];
445
- } else {
446
- NSString *detentId = [NSString stringWithFormat:@"custom-%f", value];
447
- return [UISheetPresentationControllerDetent
448
- customDetentWithIdentifier:detentId
449
- resolver:^CGFloat(id<UISheetPresentationControllerDetentResolutionContext> context) {
450
- CGFloat maxDetentValue = context.maximumDetentValue;
451
- CGFloat maxValue =
452
- self.maxHeight ? fmin(maxDetentValue, [self.maxHeight floatValue]) : maxDetentValue;
453
- return fmin(value * maxDetentValue, maxValue);
454
- }];
455
- }
509
+ NSString *detentId = [NSString stringWithFormat:@"custom-%f", value];
510
+ // Subtract bottomInset so position matches screenHeight - (detent * screenHeight)
511
+ // iOS adds bottom safe area to sheet height internally
512
+ CGFloat sheetHeight = value * self.screenHeight - self.bottomInset;
513
+ return [self customDetentWithIdentifier:detentId height:sheetHeight];
456
514
  } else if (value >= 0.5) {
457
515
  return [UISheetPresentationControllerDetent largeDetent];
458
516
  } else {
@@ -460,6 +518,18 @@
460
518
  }
461
519
  }
462
520
 
521
+ - (UISheetPresentationControllerDetent *)customDetentWithIdentifier:(NSString *)identifier
522
+ height:(CGFloat)height API_AVAILABLE(ios(16.0)) {
523
+ return [UISheetPresentationControllerDetent
524
+ customDetentWithIdentifier:identifier
525
+ resolver:^CGFloat(id<UISheetPresentationControllerDetentResolutionContext> context) {
526
+ CGFloat maxDetentValue = context.maximumDetentValue;
527
+ CGFloat maxValue =
528
+ self.maxHeight ? fmin(maxDetentValue, [self.maxHeight floatValue]) : maxDetentValue;
529
+ return fmin(height, maxValue);
530
+ }];
531
+ }
532
+
463
533
  - (UISheetPresentationControllerDetentIdentifier)detentIdentifierForIndex:(NSInteger)index {
464
534
  UISheetPresentationController *sheet = self.sheetPresentationController;
465
535
  if (!sheet)
@@ -19,7 +19,8 @@ NS_ASSUME_NONNULL_BEGIN
19
19
 
20
20
  + (void)emit:(std::shared_ptr<const facebook::react::EventEmitter>)eventEmitter
21
21
  index:(NSInteger)index
22
- position:(CGFloat)position;
22
+ position:(CGFloat)position
23
+ detent:(CGFloat)detent;
23
24
 
24
25
  @end
25
26
 
@@ -14,7 +14,8 @@
14
14
 
15
15
  + (void)emit:(std::shared_ptr<const facebook::react::EventEmitter>)eventEmitter
16
16
  index:(NSInteger)index
17
- position:(CGFloat)position {
17
+ position:(CGFloat)position
18
+ detent:(CGFloat)detent {
18
19
  if (!eventEmitter)
19
20
  return;
20
21
 
@@ -22,6 +23,7 @@
22
23
  TrueSheetViewEventEmitter::OnDetentChange event;
23
24
  event.index = static_cast<int>(index);
24
25
  event.position = static_cast<double>(position);
26
+ event.detent = static_cast<double>(detent);
25
27
  emitter->onDetentChange(event);
26
28
  }
27
29
 
@@ -0,0 +1,26 @@
1
+ //
2
+ // Created by Jovanni Lo (@lodev09)
3
+ // Copyright (c) 2024-present. All rights reserved.
4
+ //
5
+ // This source code is licensed under the MIT license found in the
6
+ // LICENSE file in the root directory of this source tree.
7
+ //
8
+
9
+ #ifdef RCT_NEW_ARCH_ENABLED
10
+
11
+ #import <Foundation/Foundation.h>
12
+ #import <react/renderer/components/TrueSheetSpec/EventEmitters.h>
13
+
14
+ using namespace facebook::react;
15
+
16
+ NS_ASSUME_NONNULL_BEGIN
17
+
18
+ @interface OnDidBlurEvent : NSObject
19
+
20
+ + (void)emit:(std::shared_ptr<const facebook::react::EventEmitter>)eventEmitter;
21
+
22
+ @end
23
+
24
+ NS_ASSUME_NONNULL_END
25
+
26
+ #endif // RCT_NEW_ARCH_ENABLED
@@ -0,0 +1,25 @@
1
+ //
2
+ // Created by Jovanni Lo (@lodev09)
3
+ // Copyright (c) 2024-present. All rights reserved.
4
+ //
5
+ // This source code is licensed under the MIT license found in the
6
+ // LICENSE file in the root directory of this source tree.
7
+ //
8
+
9
+ #ifdef RCT_NEW_ARCH_ENABLED
10
+
11
+ #import "OnDidBlurEvent.h"
12
+
13
+ @implementation OnDidBlurEvent
14
+
15
+ + (void)emit:(std::shared_ptr<const facebook::react::EventEmitter>)eventEmitter {
16
+ if (!eventEmitter)
17
+ return;
18
+
19
+ auto emitter = std::static_pointer_cast<TrueSheetViewEventEmitter const>(eventEmitter);
20
+ emitter->onDidBlur({});
21
+ }
22
+
23
+ @end
24
+
25
+ #endif // RCT_NEW_ARCH_ENABLED