@lodev09/react-native-true-sheet 4.0.0-beta.1 → 4.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 (221) hide show
  1. package/README.md +25 -4
  2. package/android/build.gradle +22 -1
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +24 -7
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +211 -182
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +5 -1
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +14 -19
  7. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +106 -0
  8. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayViewManager.kt +45 -0
  9. package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +2 -1
  10. package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +18 -0
  11. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +82 -39
  12. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +275 -88
  13. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +26 -1
  14. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +75 -3
  15. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +56 -34
  16. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetOverlayRootView.kt +137 -0
  17. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +15 -0
  18. package/android/src/main/java/com/lodev09/truesheet/utils/KeyboardUtils.kt +1 -1
  19. package/android/src/main/java/com/lodev09/truesheet/utils/TouchEventDeduper.kt +31 -0
  20. package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +19 -7
  21. package/android/src/main/jni/TrueSheetSpec.h +1 -0
  22. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +0 -8
  23. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +54 -29
  24. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +6 -1
  25. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +1 -1
  26. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +6 -4
  27. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +7 -0
  28. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetLayoutUtils.h +46 -0
  29. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewComponentDescriptor.h +21 -0
  30. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.cpp +17 -0
  31. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.h +36 -0
  32. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +4 -35
  33. package/ios/TrueSheetContainerView.h +14 -7
  34. package/ios/TrueSheetContainerView.mm +10 -7
  35. package/ios/TrueSheetContentView.h +31 -15
  36. package/ios/TrueSheetContentView.mm +133 -179
  37. package/ios/TrueSheetFooterView.h +6 -0
  38. package/ios/TrueSheetFooterView.mm +48 -2
  39. package/ios/TrueSheetModule.h +2 -7
  40. package/ios/TrueSheetModule.mm +7 -16
  41. package/ios/TrueSheetOverlayView.h +32 -0
  42. package/ios/TrueSheetOverlayView.mm +161 -0
  43. package/ios/TrueSheetView.mm +171 -29
  44. package/ios/TrueSheetViewController.h +10 -0
  45. package/ios/TrueSheetViewController.mm +256 -74
  46. package/ios/core/TrueSheetGrabberView.mm +13 -3
  47. package/ios/core/TrueSheetOverlayContainerView.h +31 -0
  48. package/ios/core/TrueSheetOverlayContainerView.mm +35 -0
  49. package/ios/events/TrueSheetLifecycleEvents.h +2 -0
  50. package/ios/events/TrueSheetLifecycleEvents.mm +8 -0
  51. package/ios/tvos/TrueSheetStubs.mm +12 -0
  52. package/ios/utils/UIView+ScrollEdgeInteraction.h +1 -0
  53. package/ios/utils/UIView+ScrollEdgeInteraction.mm +24 -2
  54. package/lib/module/TrueSheet.js +149 -34
  55. package/lib/module/TrueSheet.js.map +1 -1
  56. package/lib/module/TrueSheet.web.js +122 -53
  57. package/lib/module/TrueSheet.web.js.map +1 -1
  58. package/lib/module/TrueSheetOverlay.js +30 -0
  59. package/lib/module/TrueSheetOverlay.js.map +1 -0
  60. package/lib/module/TrueSheetOverlay.web.js +35 -0
  61. package/lib/module/TrueSheetOverlay.web.js.map +1 -0
  62. package/lib/module/TrueSheetPeek.web.js +3 -3
  63. package/lib/module/TrueSheetPeek.web.js.map +1 -1
  64. package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  65. package/lib/module/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  66. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +5 -0
  67. package/lib/module/index.js +1 -0
  68. package/lib/module/index.js.map +1 -1
  69. package/lib/module/mocks/index.js +10 -0
  70. package/lib/module/mocks/index.js.map +1 -1
  71. package/lib/module/mocks/navigation.js +31 -21
  72. package/lib/module/mocks/navigation.js.map +1 -1
  73. package/lib/module/mocks/navigationExpoRouter.js +21 -0
  74. package/lib/module/mocks/navigationExpoRouter.js.map +1 -0
  75. package/lib/module/mocks/reanimated.js +1 -1
  76. package/lib/module/mocks/reanimated.js.map +1 -1
  77. package/lib/module/navigation/{TrueSheetView.js → TrueSheetNavigatorContent.js} +14 -12
  78. package/lib/module/navigation/TrueSheetNavigatorContent.js.map +1 -0
  79. package/lib/module/navigation/actions.js +50 -0
  80. package/lib/module/navigation/actions.js.map +1 -0
  81. package/lib/module/navigation/base-types.js +2 -0
  82. package/lib/module/navigation/base-types.js.map +1 -0
  83. package/lib/module/navigation/{TrueSheetRouter.js → createTrueSheetRouter.js} +11 -24
  84. package/lib/module/navigation/createTrueSheetRouter.js.map +1 -0
  85. package/lib/module/navigation/expo-router/base-types.js +2 -0
  86. package/lib/module/navigation/expo-router/base-types.js.map +1 -0
  87. package/lib/module/navigation/expo-router/index.js +50 -0
  88. package/lib/module/navigation/expo-router/index.js.map +1 -0
  89. package/lib/module/navigation/expo-router/types.js +4 -0
  90. package/lib/module/navigation/expo-router/types.js.map +1 -0
  91. package/lib/module/navigation/index.js +43 -2
  92. package/lib/module/navigation/index.js.map +1 -1
  93. package/lib/module/navigation/navigator.js +10 -0
  94. package/lib/module/navigation/navigator.js.map +1 -0
  95. package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js +2 -2
  96. package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js.map +1 -1
  97. package/lib/module/navigation/screen/TrueSheetScreen.js +2 -2
  98. package/lib/module/navigation/screen/TrueSheetScreen.js.map +1 -1
  99. package/lib/module/navigation/screen/useSheetScreenState.js +5 -4
  100. package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -1
  101. package/lib/module/navigation/useTrueSheetNavigation.js +1 -1
  102. package/lib/module/navigation/useTrueSheetNavigation.js.map +1 -1
  103. package/lib/module/web/dom.js +10 -0
  104. package/lib/module/web/dom.js.map +1 -0
  105. package/lib/module/web/vaul/constants.js +4 -0
  106. package/lib/module/web/vaul/constants.js.map +1 -1
  107. package/lib/module/web/vaul/context.js.map +1 -1
  108. package/lib/module/web/vaul/helpers.js +10 -4
  109. package/lib/module/web/vaul/helpers.js.map +1 -1
  110. package/lib/module/web/vaul/index.js +35 -2
  111. package/lib/module/web/vaul/index.js.map +1 -1
  112. package/lib/typescript/src/TrueSheet.d.ts +6 -0
  113. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  114. package/lib/typescript/src/TrueSheet.types.d.ts +72 -1
  115. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  116. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  117. package/lib/typescript/src/TrueSheetOverlay.d.ts +8 -0
  118. package/lib/typescript/src/TrueSheetOverlay.d.ts.map +1 -0
  119. package/lib/typescript/src/TrueSheetOverlay.web.d.ts +8 -0
  120. package/lib/typescript/src/TrueSheetOverlay.web.d.ts.map +1 -0
  121. package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
  122. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts +6 -0
  123. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts.map +1 -0
  124. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +4 -0
  125. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  126. package/lib/typescript/src/index.d.ts +1 -0
  127. package/lib/typescript/src/index.d.ts.map +1 -1
  128. package/lib/typescript/src/mocks/index.d.ts +4 -0
  129. package/lib/typescript/src/mocks/index.d.ts.map +1 -1
  130. package/lib/typescript/src/mocks/navigation.d.ts +5 -1
  131. package/lib/typescript/src/mocks/navigation.d.ts.map +1 -1
  132. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +22 -0
  133. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -0
  134. package/lib/typescript/src/mocks/reanimated.d.ts.map +1 -1
  135. package/lib/typescript/src/navigation/TrueSheetNavigatorContent.d.ts +5 -0
  136. package/lib/typescript/src/navigation/TrueSheetNavigatorContent.d.ts.map +1 -0
  137. package/lib/typescript/src/navigation/actions.d.ts +71 -0
  138. package/lib/typescript/src/navigation/actions.d.ts.map +1 -0
  139. package/lib/typescript/src/navigation/base-types.d.ts +9 -0
  140. package/lib/typescript/src/navigation/base-types.d.ts.map +1 -0
  141. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +3 -0
  142. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -0
  143. package/lib/typescript/src/navigation/expo-router/base-types.d.ts +8 -0
  144. package/lib/typescript/src/navigation/expo-router/base-types.d.ts.map +1 -0
  145. package/lib/typescript/src/navigation/expo-router/index.d.ts +37 -0
  146. package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -0
  147. package/lib/typescript/src/navigation/expo-router/types.d.ts +44 -0
  148. package/lib/typescript/src/navigation/expo-router/types.d.ts.map +1 -0
  149. package/lib/typescript/src/navigation/index.d.ts +42 -3
  150. package/lib/typescript/src/navigation/index.d.ts.map +1 -1
  151. package/lib/typescript/src/navigation/navigator.d.ts +7 -0
  152. package/lib/typescript/src/navigation/navigator.d.ts.map +1 -0
  153. package/lib/typescript/src/navigation/screen/ReanimatedTrueSheetScreen.d.ts +1 -1
  154. package/lib/typescript/src/navigation/screen/ReanimatedTrueSheetScreen.d.ts.map +1 -1
  155. package/lib/typescript/src/navigation/screen/TrueSheetScreen.d.ts +1 -1
  156. package/lib/typescript/src/navigation/screen/TrueSheetScreen.d.ts.map +1 -1
  157. package/lib/typescript/src/navigation/screen/types.d.ts +3 -4
  158. package/lib/typescript/src/navigation/screen/types.d.ts.map +1 -1
  159. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +5 -6
  160. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -1
  161. package/lib/typescript/src/navigation/types.d.ts +65 -5
  162. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  163. package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts +1 -1
  164. package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts.map +1 -1
  165. package/lib/typescript/src/web/dom.d.ts +7 -0
  166. package/lib/typescript/src/web/dom.d.ts.map +1 -0
  167. package/lib/typescript/src/web/vaul/constants.d.ts +1 -0
  168. package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -1
  169. package/lib/typescript/src/web/vaul/context.d.ts +1 -0
  170. package/lib/typescript/src/web/vaul/context.d.ts.map +1 -1
  171. package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
  172. package/lib/typescript/src/web/vaul/index.d.ts +7 -1
  173. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
  174. package/package.json +32 -9
  175. package/react-native.config.js +1 -0
  176. package/src/TrueSheet.tsx +184 -46
  177. package/src/TrueSheet.types.ts +77 -1
  178. package/src/TrueSheet.web.tsx +135 -64
  179. package/src/TrueSheetOverlay.tsx +24 -0
  180. package/src/TrueSheetOverlay.web.tsx +33 -0
  181. package/src/TrueSheetPeek.web.tsx +4 -3
  182. package/src/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  183. package/src/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  184. package/src/fabric/TrueSheetViewNativeComponent.ts +5 -0
  185. package/src/index.ts +1 -0
  186. package/src/mocks/index.ts +7 -0
  187. package/src/mocks/navigation.ts +32 -25
  188. package/src/mocks/navigationExpoRouter.ts +41 -0
  189. package/src/mocks/reanimated.ts +8 -9
  190. package/src/navigation/{TrueSheetView.tsx → TrueSheetNavigatorContent.tsx} +24 -17
  191. package/src/navigation/actions.ts +78 -0
  192. package/src/navigation/base-types.ts +20 -0
  193. package/src/navigation/createTrueSheetRouter.ts +194 -0
  194. package/src/navigation/expo-router/base-types.ts +18 -0
  195. package/src/navigation/expo-router/index.ts +90 -0
  196. package/src/navigation/expo-router/types.ts +116 -0
  197. package/src/navigation/index.ts +68 -2
  198. package/src/navigation/navigator.ts +18 -0
  199. package/src/navigation/screen/ReanimatedTrueSheetScreen.tsx +2 -2
  200. package/src/navigation/screen/TrueSheetScreen.tsx +2 -2
  201. package/src/navigation/screen/types.ts +4 -6
  202. package/src/navigation/screen/useSheetScreenState.ts +10 -15
  203. package/src/navigation/types.ts +99 -16
  204. package/src/navigation/useTrueSheetNavigation.ts +1 -1
  205. package/src/web/dom.ts +9 -0
  206. package/src/web/vaul/constants.ts +4 -0
  207. package/src/web/vaul/context.ts +1 -0
  208. package/src/web/vaul/helpers.ts +13 -6
  209. package/src/web/vaul/index.tsx +46 -1
  210. package/lib/module/navigation/TrueSheetRouter.js.map +0 -1
  211. package/lib/module/navigation/TrueSheetView.js.map +0 -1
  212. package/lib/module/navigation/createTrueSheetNavigator.js +0 -63
  213. package/lib/module/navigation/createTrueSheetNavigator.js.map +0 -1
  214. package/lib/typescript/src/navigation/TrueSheetRouter.d.ts +0 -57
  215. package/lib/typescript/src/navigation/TrueSheetRouter.d.ts.map +0 -1
  216. package/lib/typescript/src/navigation/TrueSheetView.d.ts +0 -10
  217. package/lib/typescript/src/navigation/TrueSheetView.d.ts.map +0 -1
  218. package/lib/typescript/src/navigation/createTrueSheetNavigator.d.ts +0 -35
  219. package/lib/typescript/src/navigation/createTrueSheetNavigator.d.ts.map +0 -1
  220. package/src/navigation/TrueSheetRouter.ts +0 -234
  221. package/src/navigation/createTrueSheetNavigator.tsx +0 -93
@@ -45,6 +45,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
45
45
  - (void)restoreWindowAccessibilityElements;
46
46
  - (void)setSheetAccessibilityElementsHidden:(BOOL)hidden;
47
47
  - (void)setAccessibilityContentElement:(UIView *)contentView;
48
+ - (NSArray *)accessibilityGrabberElements;
48
49
  - (void)endInteractiveDismissState;
49
50
  - (void)animateInteractiveContainerToTransform:(CGAffineTransform)transform
50
51
  duration:(NSTimeInterval)duration
@@ -53,6 +54,20 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
53
54
 
54
55
  @end
55
56
 
57
+ // VoiceOver delivers the two-finger Z (escape) gesture by walking up the view
58
+ // hierarchy from the focused element, so the sheet's root view handles it.
59
+ @interface TrueSheetControllerView : UIView
60
+ @property (nonatomic, weak) TrueSheetViewController *controller;
61
+ @end
62
+
63
+ @implementation TrueSheetControllerView
64
+
65
+ - (BOOL)accessibilityPerformEscape {
66
+ return [self.controller accessibilityPerformEscape];
67
+ }
68
+
69
+ @end
70
+
56
71
  @implementation TrueSheetViewController {
57
72
  TrueSheetPositionState _lastEmittedPositionState;
58
73
  CGSize _lastReportedSize;
@@ -70,6 +85,11 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
70
85
  BOOL _isDragging;
71
86
  BOOL _isWillDismissEmitted;
72
87
 
88
+ // Last bottom safe area observed while presented — reused as the estimate
89
+ // for the next present (beats the float-threshold guess for re-presents).
90
+ BOOL _hasObservedBottomInset;
91
+ CGFloat _observedBottomInset;
92
+
73
93
  BOOL _isInteractiveDismiss;
74
94
  CGFloat _interactiveStartPosition;
75
95
  UIView *_interactiveContainerView;
@@ -202,10 +222,19 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
202
222
  }
203
223
 
204
224
  - (CGFloat)screenHeight {
205
- UIWindow *window = self.view.window;
225
+ // In a windowed iPad app (Stage Manager, iPadOS 26 windowing) the window can be shorter than
226
+ // UIScreen.mainScreen, so prefer the window's height. Some callers run before this controller's
227
+ // view is attached (e.g. mid-presentation detent resolution), so fall back to the presenter's.
228
+ UIWindow *window = self.view.window ?: self.presentingViewController.view.window;
206
229
  return window ? window.bounds.size.height : UIScreen.mainScreen.bounds.size.height;
207
230
  }
208
231
 
232
+ // Only phone sheets absorb the bottom safe-area inset — non-phone idioms
233
+ // (iPad form sheets) are inset from the screen edge by UIKit.
234
+ static BOOL TrueSheetIsPhoneIdiom(void) {
235
+ return UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone;
236
+ }
237
+
209
238
  - (CGFloat)detentBottomAdjustmentForHeight:(CGFloat)height {
210
239
  if (_insetAdjustment == TrueSheetViewInsetAdjustment::Automatic) {
211
240
  return 0;
@@ -215,7 +244,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
215
244
  }
216
245
 
217
246
  - (CGFloat)bottomSafeAreaForHeight:(CGFloat)height {
218
- if (UIDevice.currentDevice.userInterfaceIdiom != UIUserInterfaceIdiomPhone) {
247
+ if (!TrueSheetIsPhoneIdiom()) {
219
248
  return 0;
220
249
  }
221
250
 
@@ -232,49 +261,101 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
232
261
  }
233
262
 
234
263
  /**
235
- * Bottom inset excluded from the auto detent. A relative footer owns the
236
- * sheet's bottom edge, so it absorbs the inset UIKit lays custom detents out
237
- * above the safe area, which would otherwise gap the content from the footer.
264
+ * Largest height the sheet can reach across its configured detents, excluding
265
+ * the inset currently baked into the footer's height. Basis for the footer's
266
+ * inset absorption so it follows the same rule as bottomSafeAreaForHeight:
267
+ * a sheet that only ever floats (small detents on iOS 26) keeps its footer
268
+ * unpadded.
238
269
  */
239
- - (CGFloat)autoDetentBottomInsetForHeight:(CGFloat)height {
240
- if (_insetAdjustment != TrueSheetViewInsetAdjustment::Automatic) {
241
- // 'none' already excludes the inset for every detent
242
- return 0;
270
+ - (CGFloat)maxNaturalDetentHeight {
271
+ CGFloat applied = [self.footerHeight floatValue] > 0 ? _appliedFooterBottomInset : 0;
272
+ CGFloat maxHeight = 0;
273
+
274
+ for (id detent in self.detents) {
275
+ if (![detent isKindOfClass:[NSNumber class]]) {
276
+ continue;
277
+ }
278
+
279
+ CGFloat value = [detent doubleValue];
280
+ CGFloat height = 0;
281
+ if (value == -1) {
282
+ // Auto counts a relative footer's (padded) height
283
+ height = [_detentCalculator autoHeight] - (_absoluteFooter ? 0 : applied);
284
+ } else if (value == -2) {
285
+ // Peek counts an absolute footer's (padded) height
286
+ height = [_detentCalculator peekHeight] - (_absoluteFooter ? applied : 0);
287
+ } else if (value > 0 && value <= 1) {
288
+ height = value * self.screenHeight;
289
+ }
290
+ maxHeight = fmax(maxHeight, height);
243
291
  }
244
292
 
245
- if (_absoluteFooter || [self.footerHeight floatValue] <= 0) {
246
- return 0;
293
+ if (self.maxContentHeight) {
294
+ maxHeight = fmin(maxHeight, [self.maxContentHeight floatValue]);
247
295
  }
248
296
 
249
- return [self bottomSafeAreaForHeight:height];
297
+ return maxHeight > 0 ? maxHeight : self.screenHeight;
250
298
  }
251
299
 
252
- // The inset the footer absorbs as padding. Uses the auto detent height when
253
- // available so it matches the auto detent's inset exclusion above.
300
+ // The inset the footer absorbs as padding.
254
301
  - (CGFloat)footerBottomInset {
255
302
  if (_insetAdjustment != TrueSheetViewInsetAdjustment::Automatic) {
256
303
  return 0;
257
304
  }
258
305
 
259
- return [self bottomSafeAreaForHeight:_autoDetentHeight > 0 ? _autoDetentHeight : self.screenHeight];
306
+ // Early out so the maxNaturalDetentHeight fallback below doesn't run per
307
+ // layout pass on iPad only to be zeroed by bottomSafeAreaForHeight:.
308
+ if (!TrueSheetIsPhoneIdiom()) {
309
+ return 0;
310
+ }
311
+
312
+ // Once laid out, the sheet's own safe area is the authority — it reflects
313
+ // UIKit's actual float/anchored decision (a floating sheet gets 0), where
314
+ // bottomSafeAreaForHeight: can only guess the float threshold. Guessing
315
+ // wrong leaves the safe-area region unfilled (gap) or double-padded.
316
+ UIView *view = self.viewIfLoaded;
317
+ if (view.window) {
318
+ _hasObservedBottomInset = YES;
319
+ _observedBottomInset = view.safeAreaInsets.bottom;
320
+ return _observedBottomInset;
321
+ }
322
+
323
+ // Off-window while presented (e.g. hidden behind a native screen): the last
324
+ // observed value is this sheet's own truth — prefer it over the
325
+ // float-threshold guess. Cleared on dismiss so a re-present observes fresh
326
+ // geometry (rotation or detent changes while dismissed would stale it).
327
+ if (_hasObservedBottomInset) {
328
+ return _observedBottomInset;
329
+ }
330
+
331
+ return [self bottomSafeAreaForHeight:[self maxNaturalDetentHeight]];
260
332
  }
261
333
 
262
334
  /**
263
- * Bottom inset excluded from the peek detent. An absolute footer counts
264
- * toward the peek height and absorbs the inset as padding, but UIKit already
265
- * lays custom detents out above the safe area — subtract it so the inset
266
- * isn't doubled.
335
+ * Bottom inset excluded from the auto detent. A relative footer owns the
336
+ * sheet's bottom edge and carries the inset in its height (see
337
+ * footerBottomInset) subtract exactly what was baked in so UIKit's own
338
+ * safe-area layout doesn't double it.
267
339
  */
268
- - (CGFloat)peekDetentBottomInsetForHeight:(CGFloat)height {
269
- if (_insetAdjustment != TrueSheetViewInsetAdjustment::Automatic) {
340
+ - (CGFloat)autoDetentBottomInset {
341
+ if (_absoluteFooter || [self.footerHeight floatValue] <= 0) {
270
342
  return 0;
271
343
  }
272
344
 
345
+ return _appliedFooterBottomInset;
346
+ }
347
+
348
+ /**
349
+ * Bottom inset excluded from the peek detent. An absolute footer counts
350
+ * toward the peek height and carries the inset in its height — subtract
351
+ * exactly what was baked in so it isn't doubled.
352
+ */
353
+ - (CGFloat)peekDetentBottomInset {
273
354
  if (!_absoluteFooter || [self.footerHeight floatValue] <= 0) {
274
355
  return 0;
275
356
  }
276
357
 
277
- return [self bottomSafeAreaForHeight:height];
358
+ return _appliedFooterBottomInset;
278
359
  }
279
360
 
280
361
  - (BOOL)isDesignCompatibilityMode {
@@ -314,6 +395,12 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
314
395
 
315
396
  #pragma mark - View Lifecycle
316
397
 
398
+ - (void)loadView {
399
+ TrueSheetControllerView *view = [[TrueSheetControllerView alloc] init];
400
+ view.controller = self;
401
+ self.view = view;
402
+ }
403
+
317
404
  - (void)viewDidLoad {
318
405
  [super viewDidLoad];
319
406
  self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
@@ -421,6 +508,37 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
421
508
  }
422
509
  }
423
510
 
511
+ - (NSArray *)accessibilityGrabberElements {
512
+ if (_grabberView && !_grabberView.hidden) {
513
+ return @[ _grabberView ];
514
+ }
515
+
516
+ // The system grabber is an accessibility element UIKit adds to the presented view
517
+ // (a sibling of our root view). Expose it so VoiceOver can resize or dismiss with it.
518
+ // UIKit also keeps a transparent bottom grabber there, so skip invisible views.
519
+ NSMutableArray *elements = [NSMutableArray array];
520
+ for (UIView *subview in self.presentationController.presentedView.subviews) {
521
+ if (subview != self.view && subview.isAccessibilityElement && !subview.hidden && subview.alpha > 0) {
522
+ [elements addObject:subview];
523
+ }
524
+ }
525
+ return elements;
526
+ }
527
+
528
+ - (BOOL)accessibilityPerformEscape {
529
+ if (!_isPresented) {
530
+ return NO;
531
+ }
532
+
533
+ if (!self.dismissible) {
534
+ [self.delegate viewControllerDidAttemptDismiss];
535
+ return NO;
536
+ }
537
+
538
+ [self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
539
+ return YES;
540
+ }
541
+
424
542
  - (void)setAccessibilityContentElement:(UIView *)contentView {
425
543
  BOOL hasAccessibilityFooterElements = [contentView isKindOfClass:[TrueSheetContainerView class]] &&
426
544
  [(TrueSheetContainerView *)contentView hasAccessibilityFooterElements];
@@ -433,12 +551,12 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
433
551
  // its accessibilityElements getter recomputes live, so a footer/content subtree
434
552
  // that remounts (e.g. swapping a footer button on a state change) stays
435
553
  // discoverable instead of leaving this snapshot pointing at destroyed views.
436
- NSArray *accessibilityElements;
554
+ NSMutableArray *accessibilityElements = [[self accessibilityGrabberElements] mutableCopy];
437
555
  if (isAccessibilityModal) {
438
556
  NSArray *contentElements = contentView.accessibilityElements;
439
- accessibilityElements = contentElements.count > 0 ? contentElements : @[ contentView ];
557
+ [accessibilityElements addObjectsFromArray:contentElements.count > 0 ? contentElements : @[ contentView ]];
440
558
  } else {
441
- accessibilityElements = @[ contentView ];
559
+ [accessibilityElements addObject:contentView];
442
560
  }
443
561
 
444
562
  self.view.isAccessibilityElement = NO;
@@ -451,6 +569,15 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
451
569
  presentedView.accessibilityViewIsModal = isAccessibilityModal;
452
570
  presentedView.accessibilityElements = accessibilityElements;
453
571
 
572
+ // The system grabber is a sibling of our root view, so an escape performed while it
573
+ // is focused walks presentedView → window and never reaches TrueSheetControllerView.
574
+ if (@available(iOS 17.0, *)) {
575
+ __weak __typeof(self) weakSelf = self;
576
+ presentedView.accessibilityPerformEscapeBlock = ^BOOL {
577
+ return [weakSelf accessibilityPerformEscape];
578
+ };
579
+ }
580
+
454
581
  UIWindow *window = self.view.window;
455
582
  UIViewController *presentingViewController = [self accessibilityPresentingViewController];
456
583
  if (window && presentingViewController.view) {
@@ -520,6 +647,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
520
647
  [self restoreWindowAccessibilityElements];
521
648
  _isPresented = NO;
522
649
  _isWillDismissEmitted = NO;
650
+ _hasObservedBottomInset = NO;
523
651
 
524
652
  [_anchorView removeFromSuperview];
525
653
  _anchorView = nil;
@@ -575,24 +703,26 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
575
703
  return;
576
704
  }
577
705
 
578
- if (_pendingContentSizeChange || _pendingDetentsChange) {
706
+ // Mid-drag the frame tracks the finger — never settle or learn from it;
707
+ // pending changes keep their flags and settle on the first post-drag layout.
708
+ if ((_pendingContentSizeChange || _pendingDetentsChange) && !_isDragging) {
579
709
  _pendingContentSizeChange = NO;
580
710
  _pendingDetentsChange = NO;
581
711
  [self settleAtDetentIndex:self.currentDetentIndex debug:@"layout"];
582
- } else if (!_isDragging) {
583
- // Drags emit from the pan handler. A child on top moves this sheet two
584
- // ways: an animated collapse/restore step (presenting/dismissing) — emit
585
- // the target non-realtime so JS animates to it — or direct per-frame
586
- // re-layouts while the child drags, which stay realtime.
712
+ } else {
713
+ // A child on top moves this sheet two ways: an animated collapse/restore
714
+ // step (presenting/dismissing) — emit the target non-realtime so JS
715
+ // animates to it — or direct per-frame re-layouts while the child drags,
716
+ // which stay realtime. Mid-drag no child is on top, so drags emit realtime.
587
717
  BOOL realtime = self.presentedViewController == nil || !self.isPresentedViewAnimating;
588
718
 
589
719
  // A layout pass at rest can nudge the frame by a sub-pixel amount
590
720
  // (pixel-grid snapping after present), leaving the interpolated index
591
721
  // slightly off the whole number. Re-learn when the frame is effectively
592
722
  // at the current detent so the drift is absorbed instead of emitted.
593
- // The tight threshold keeps real movement (e.g. scroll-driven expansion)
594
- // from being absorbed.
595
- if (self.presentedViewController == nil && !self.isPresentedViewAnimating) {
723
+ // The tight threshold keeps real movement (e.g. scroll-driven expansion
724
+ // or a drag hovering near the detent) from being absorbed.
725
+ if (!_isDragging && self.presentedViewController == nil && !self.isPresentedViewAnimating) {
596
726
  NSInteger index = self.currentDetentIndex;
597
727
  CGFloat expectedHeight = [_detentCalculator resolvedHeightForIndex:index];
598
728
  CGFloat actualHeight = self.screenHeight - self.currentPosition;
@@ -605,9 +735,46 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
605
735
  }
606
736
  }
607
737
 
738
+ // Rotation or an iPad window resize changes the height fractional detents
739
+ // resolve against — re-resolve alongside the transition so the sheet lands on
740
+ // the new height instead of waiting for a content-driven rebuild.
741
+ - (void)viewWillTransitionToSize:(CGSize)size
742
+ withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
743
+ [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
744
+
745
+ if (@available(iOS 16.0, *)) {
746
+ [coordinator
747
+ animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
748
+ [self.sheet invalidateDetents];
749
+ }
750
+ completion:nil];
751
+ }
752
+ }
753
+
754
+ // Fires when UIKit resolves the sheet's float/anchored placement (present,
755
+ // detent resize crossing the float threshold, rotation). The footer's
756
+ // absorbed inset follows the observed safe area — repush it right away so
757
+ // the footer pads/unpads in the same layout pass.
758
+ - (void)viewSafeAreaInsetsDidChange {
759
+ [super viewSafeAreaInsetsDidChange];
760
+
761
+ // Mid-dismiss the frame moves off-screen and reports transient insets —
762
+ // don't let them pollute the observed value (matches viewDidLayoutSubviews).
763
+ if (!self.isBeingDismissed) {
764
+ [self.delegate viewControllerSafeAreaInsetsDidChange];
765
+ }
766
+ }
767
+
608
768
  - (void)viewDidLayoutSubviews {
609
769
  [super viewDidLayoutSubviews];
610
770
 
771
+ // viewSafeAreaInsetsDidChange doesn't fire on re-present when the observed
772
+ // inset matches the previous presentation's — refresh every layout pass
773
+ // instead (deduped downstream, so unchanged values are free).
774
+ if (!self.isBeingDismissed) {
775
+ [self.delegate viewControllerSafeAreaInsetsDidChange];
776
+ }
777
+
611
778
  // Skip size reports while backgrounded behind a stacked child — the push-back
612
779
  // scaling changes the frame and would needlessly resize content.
613
780
  // _lastReportedSize stays stale so the restore pass re-reports any real change.
@@ -695,7 +862,14 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
695
862
  _isDragging = YES;
696
863
  break;
697
864
  case UIGestureRecognizerStateChanged:
698
- [self emitChangePositionDelegateWithPosition:self.currentPosition realtime:YES debug:@"drag change"];
865
+ // Layout owns the emit while the sheet resizes with the finger (a
866
+ // layout pass is already queued), and the transition tick owns it
867
+ // during an interactive dismiss. Translation-only movement (e.g.
868
+ // dragging down to the first detent) triggers neither — emit here as
869
+ // the backup.
870
+ if (!_isTransitioning && !self.presentedView.layer.needsLayout && !self.viewIfLoaded.layer.needsLayout) {
871
+ [self emitChangePositionDelegateWithPosition:self.currentPosition realtime:YES debug:@"drag change"];
872
+ }
699
873
  break;
700
874
  case UIGestureRecognizerStateEnded:
701
875
  case UIGestureRecognizerStateCancelled: {
@@ -780,27 +954,26 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
780
954
  }
781
955
 
782
956
  - (void)handleTransitionTick {
783
- // Interactive dismiss phase (finger down) — the pan handler emits.
784
- if (!_isDragging) {
785
- CGFloat position = self.livePosition;
786
-
787
- if (self.isBeingDismissed) {
788
- // Emit only once the dismiss is committed: on release UIKit moves the
789
- // model frame off-screen, while a cancelled drag rewinds it back to the
790
- // detent (isBeingDismissed stays YES during the rewind).
791
- if (self.currentPosition >= self.screenHeight) {
792
- [self emitWillDismissEvents];
793
- }
957
+ // Also covers the interactive dismiss phase (finger down) — layout passes
958
+ // are skipped while transitioning, so the tick owns position there too.
959
+ CGFloat position = self.livePosition;
794
960
 
795
- // Hide blur at the end of dismiss to prevent UIVisualEffectView
796
- // from causing a flicker/flash at the bottom edge of the sheet.
797
- if (self.screenHeight - position < 1) {
798
- _blurView.alpha = 0;
799
- }
961
+ if (self.isBeingDismissed) {
962
+ // Emit only once the dismiss is committed: on release UIKit moves the
963
+ // model frame off-screen, while a cancelled drag rewinds it back to the
964
+ // detent (isBeingDismissed stays YES during the rewind).
965
+ if (self.currentPosition >= self.screenHeight) {
966
+ [self emitWillDismissEvents];
800
967
  }
801
968
 
802
- [self emitChangePositionDelegateWithPosition:position realtime:YES debug:@"transition"];
969
+ // Hide blur at the end of dismiss to prevent UIVisualEffectView
970
+ // from causing a flicker/flash at the bottom edge of the sheet.
971
+ if (self.screenHeight - position < 1) {
972
+ _blurView.alpha = 0;
973
+ }
803
974
  }
975
+
976
+ [self emitChangePositionDelegateWithPosition:position realtime:YES debug:@"transition"];
804
977
  }
805
978
 
806
979
  /**
@@ -1079,8 +1252,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
1079
1252
  return [self customDetentWithIdentifier:@"custom-auto"
1080
1253
  atIndex:index
1081
1254
  heightBlock:^CGFloat {
1082
- CGFloat height = self->_autoDetentHeight;
1083
- return height - [self autoDetentBottomInsetForHeight:height];
1255
+ return self->_autoDetentHeight - [self autoDetentBottomInset];
1084
1256
  }];
1085
1257
  } else {
1086
1258
  return [UISheetPresentationControllerDetent mediumDetent];
@@ -1092,8 +1264,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
1092
1264
  return [self customDetentWithIdentifier:@"custom-peek"
1093
1265
  atIndex:index
1094
1266
  heightBlock:^CGFloat {
1095
- CGFloat height = self->_peekDetentHeight;
1096
- return height - [self peekDetentBottomInsetForHeight:height];
1267
+ return self->_peekDetentHeight - [self peekDetentBottomInset];
1097
1268
  }];
1098
1269
  } else {
1099
1270
  return [UISheetPresentationControllerDetent mediumDetent];
@@ -1107,8 +1278,13 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
1107
1278
 
1108
1279
  if (@available(iOS 16.0, *)) {
1109
1280
  NSString *detentId = [NSString stringWithFormat:@"custom-%f", value];
1110
- CGFloat sheetHeight = value * self.screenHeight;
1111
- return [self customDetentWithIdentifier:detentId height:sheetHeight atIndex:index];
1281
+ // Resolve lazily detents are built before presentation (no window yet), and
1282
+ // UIKit re-resolves on container size changes (rotation, windowed iPad resize).
1283
+ return [self customDetentWithIdentifier:detentId
1284
+ atIndex:index
1285
+ heightBlock:^CGFloat {
1286
+ return value * self.screenHeight;
1287
+ }];
1112
1288
  } else if (value >= 0.5) {
1113
1289
  return [UISheetPresentationControllerDetent largeDetent];
1114
1290
  } else {
@@ -1116,16 +1292,6 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
1116
1292
  }
1117
1293
  }
1118
1294
 
1119
- - (UISheetPresentationControllerDetent *)customDetentWithIdentifier:(NSString *)identifier
1120
- height:(CGFloat)height
1121
- atIndex:(NSInteger)index API_AVAILABLE(ios(16.0)) {
1122
- return [self customDetentWithIdentifier:identifier
1123
- atIndex:index
1124
- heightBlock:^CGFloat {
1125
- return height;
1126
- }];
1127
- }
1128
-
1129
1295
  - (UISheetPresentationControllerDetent *)customDetentWithIdentifier:(NSString *)identifier
1130
1296
  atIndex:(NSInteger)index
1131
1297
  heightBlock:(CGFloat (^)(void))heightBlock
@@ -1246,14 +1412,11 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
1246
1412
  #if RNTS_IPHONE_OS_VERSION_AVAILABLE(26_1) && !TARGET_OS_MACCATALYST
1247
1413
  if (@available(iOS 26.1, *)) {
1248
1414
  if (!self.isDesignCompatibilityMode) {
1249
- if (self.backgroundColor) {
1250
- self.sheet.backgroundEffect = [UIColorEffect effectWithColor:self.backgroundColor];
1251
- } else if (hasBlur) {
1415
+ if (self.backgroundColor || hasBlur) {
1252
1416
  self.sheet.backgroundEffect = [UIColorEffect effectWithColor:[UIColor clearColor]];
1253
1417
  } else {
1254
1418
  self.sheet.backgroundEffect = nil;
1255
1419
  }
1256
- return;
1257
1420
  }
1258
1421
  }
1259
1422
  #endif
@@ -1305,6 +1468,8 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
1305
1468
  }];
1306
1469
  } else if (strongSelf.dismissible) {
1307
1470
  [strongSelf.presentingViewController dismissViewControllerAnimated:YES completion:nil];
1471
+ } else {
1472
+ [strongSelf.delegate viewControllerDidAttemptDismiss];
1308
1473
  }
1309
1474
  };
1310
1475
 
@@ -1316,6 +1481,12 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
1316
1481
  _grabberView.onIncrement = nil;
1317
1482
  _grabberView.onDecrement = nil;
1318
1483
  }
1484
+
1485
+ // Only the topmost sheet may refresh, or a parent's prop update would steal the
1486
+ // window accessibility override from the child presented over it.
1487
+ if (_isPresented && self.isTopmostPresentedController) {
1488
+ [self setupAccessibilityContainer];
1489
+ }
1319
1490
  }
1320
1491
 
1321
1492
  - (void)handleGrabberTap {
@@ -1328,8 +1499,12 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
1328
1499
  return;
1329
1500
 
1330
1501
  NSInteger nextIndex = (currentIndex + 1) % detentCount;
1331
- if (nextIndex == 0 && detentCount == 1 && self.dismissible) {
1332
- [self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
1502
+ if (nextIndex == 0 && detentCount == 1) {
1503
+ if (self.dismissible) {
1504
+ [self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
1505
+ } else {
1506
+ [self.delegate viewControllerDidAttemptDismiss];
1507
+ }
1333
1508
  } else {
1334
1509
  [self.sheet animateChanges:^{
1335
1510
  [self resizeToDetentIndex:nextIndex];
@@ -1421,6 +1596,13 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
1421
1596
  return self.dismissible;
1422
1597
  }
1423
1598
 
1599
+ // UIKit calls this after shouldDismiss returns NO — the sheet has already
1600
+ // bounced back, so JS can confirm and dismiss programmatically (Apple's
1601
+ // unsaved-changes pattern).
1602
+ - (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)presentationController {
1603
+ [self.delegate viewControllerDidAttemptDismiss];
1604
+ }
1605
+
1424
1606
  - (void)sheetPresentationControllerDidChangeSelectedDetentIdentifier:
1425
1607
  (UISheetPresentationController *)sheetPresentationController {
1426
1608
  dispatch_async(dispatch_get_main_queue(), ^{
@@ -91,6 +91,13 @@ static const CGFloat kHitPaddingVertical = 10.0;
91
91
  }
92
92
  }
93
93
 
94
+ // Pad the pill evenly like the system grabber. The view frame itself stays inside
95
+ // the sheet, since the reveal check reads the frame rather than this rect.
96
+ - (CGRect)accessibilityFrame {
97
+ CGRect rect = CGRectInset(_vibrancyView.frame, -kHitPaddingHorizontal, -kHitPaddingVertical);
98
+ return UIAccessibilityConvertFrameToScreenCoordinates(rect, self);
99
+ }
100
+
94
101
  - (void)accessibilityIncrement {
95
102
  if (_onIncrement) {
96
103
  _onIncrement();
@@ -144,14 +151,17 @@ static const CGFloat kHitPaddingVertical = 10.0;
144
151
  CGFloat topMargin = [self effectiveTopMargin];
145
152
  CGFloat parentWidth = self.superview ? self.superview.bounds.size.width : UIScreen.mainScreen.bounds.size.width;
146
153
 
154
+ // Keep the frame inside the sheet. A frame above the top edge reads as a clipped element
155
+ // to VoiceOver, and the sheet reveals it on focus by moving to the next detent.
147
156
  CGFloat frameWidth = pillWidth + kHitPaddingHorizontal * 2;
148
- CGFloat frameHeight = pillHeight + kHitPaddingVertical * 2;
149
- CGFloat frameY = topMargin - kHitPaddingVertical;
157
+ CGFloat frameY = MAX(0, topMargin - kHitPaddingVertical);
158
+ CGFloat pillY = topMargin - frameY;
159
+ CGFloat frameHeight = pillY + pillHeight + kHitPaddingVertical;
150
160
 
151
161
  self.frame = CGRectMake((parentWidth - frameWidth) / 2.0, frameY, frameWidth, frameHeight);
152
162
  self.backgroundColor = UIColor.clearColor;
153
163
 
154
- CGRect pillRect = CGRectMake(kHitPaddingHorizontal, kHitPaddingVertical, pillWidth, pillHeight);
164
+ CGRect pillRect = CGRectMake(kHitPaddingHorizontal, pillY, pillWidth, pillHeight);
155
165
  _vibrancyView.frame = pillRect;
156
166
  _vibrancyView.layer.cornerRadius = [self effectiveCornerRadius];
157
167
  _vibrancyView.clipsToBounds = YES;
@@ -0,0 +1,31 @@
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 <UIKit/UIKit.h>
12
+
13
+ NS_ASSUME_NONNULL_BEGIN
14
+
15
+ @protocol TrueSheetOverlayContainerViewDelegate <NSObject>
16
+ - (void)overlayContainerViewDidLayout:(CGSize)size;
17
+ @end
18
+
19
+ /**
20
+ * Window-level view hosting an overlay's children.
21
+ * Touches that miss every child fall through to the views beneath.
22
+ */
23
+ @interface TrueSheetOverlayContainerView : UIView
24
+
25
+ @property (nonatomic, weak, nullable) id<TrueSheetOverlayContainerViewDelegate> delegate;
26
+
27
+ @end
28
+
29
+ NS_ASSUME_NONNULL_END
30
+
31
+ #endif
@@ -0,0 +1,35 @@
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 "TrueSheetOverlayContainerView.h"
12
+
13
+ @implementation TrueSheetOverlayContainerView
14
+
15
+ - (instancetype)initWithFrame:(CGRect)frame {
16
+ if (self = [super initWithFrame:frame]) {
17
+ self.backgroundColor = UIColor.clearColor;
18
+ self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
19
+ }
20
+ return self;
21
+ }
22
+
23
+ - (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
24
+ UIView *hit = [super hitTest:point withEvent:event];
25
+ return hit == self ? nil : hit;
26
+ }
27
+
28
+ - (void)layoutSubviews {
29
+ [super layoutSubviews];
30
+ [self.delegate overlayContainerViewDidLayout:self.bounds.size];
31
+ }
32
+
33
+ @end
34
+
35
+ #endif
@@ -33,6 +33,8 @@ NS_ASSUME_NONNULL_BEGIN
33
33
 
34
34
  + (void)emitDidDismiss:(std::shared_ptr<const facebook::react::EventEmitter>)eventEmitter;
35
35
 
36
+ + (void)emitDismissAttempt:(std::shared_ptr<const facebook::react::EventEmitter>)eventEmitter;
37
+
36
38
  @end
37
39
 
38
40
  NS_ASSUME_NONNULL_END
@@ -66,6 +66,14 @@
66
66
  emitter->onDidDismiss({});
67
67
  }
68
68
 
69
+ + (void)emitDismissAttempt:(std::shared_ptr<const facebook::react::EventEmitter>)eventEmitter {
70
+ if (!eventEmitter)
71
+ return;
72
+
73
+ auto emitter = std::static_pointer_cast<TrueSheetViewEventEmitter const>(eventEmitter);
74
+ emitter->onDismissAttempt({});
75
+ }
76
+
69
77
  @end
70
78
 
71
79
  #endif // RCT_NEW_ARCH_ENABLED