@lodev09/react-native-true-sheet 4.0.0-beta.1 → 4.0.0-beta.11

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 +52 -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 +148 -188
  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
@@ -18,8 +18,9 @@
18
18
 
19
19
  #import <TrueSheetSpec/TrueSheetSpec.h>
20
20
 
21
- // Static registry to store view references by tag
22
- static NSMutableDictionary<NSNumber *, TrueSheetView *> *viewRegistry;
21
+ // Static registry of live hosts by tag — weak so an unmounted host deallocates
22
+ // (RN zeroes the tag before teardown, so hosts can't unregister themselves)
23
+ static NSMapTable<NSNumber *, TrueSheetView *> *viewRegistry;
23
24
 
24
25
  @interface TrueSheetModule () <NativeTrueSheetModuleSpec>
25
26
  @end
@@ -30,7 +31,7 @@ RCT_EXPORT_MODULE(TrueSheetModule)
30
31
 
31
32
  + (void)initialize {
32
33
  if (self == [TrueSheetModule class]) {
33
- viewRegistry = [NSMutableDictionary new];
34
+ viewRegistry = [NSMapTable strongToWeakObjectsMapTable];
34
35
  }
35
36
  }
36
37
 
@@ -151,7 +152,7 @@ RCT_EXPORT_MODULE(TrueSheetModule)
151
152
  // Find the root presented sheet (one without a parent TrueSheet)
152
153
  TrueSheetView *rootSheet = nil;
153
154
 
154
- for (TrueSheetView *view in viewRegistry.allValues) {
155
+ for (TrueSheetView *view in viewRegistry.objectEnumerator) {
155
156
  if (!view.viewController.isPresented) {
156
157
  continue;
157
158
  }
@@ -191,7 +192,7 @@ RCT_EXPORT_MODULE(TrueSheetModule)
191
192
  }
192
193
 
193
194
  @synchronized(viewRegistry) {
194
- return viewRegistry[reactTag];
195
+ return [viewRegistry objectForKey:reactTag];
195
196
  }
196
197
  }
197
198
 
@@ -201,17 +202,7 @@ RCT_EXPORT_MODULE(TrueSheetModule)
201
202
  }
202
203
 
203
204
  @synchronized(viewRegistry) {
204
- viewRegistry[tag] = view;
205
- }
206
- }
207
-
208
- + (void)unregisterViewWithTag:(NSNumber *)tag {
209
- if (!tag) {
210
- return;
211
- }
212
-
213
- @synchronized(viewRegistry) {
214
- [viewRegistry removeObjectForKey:tag];
205
+ [viewRegistry setObject:view forKey:tag];
215
206
  }
216
207
  }
217
208
 
@@ -0,0 +1,32 @@
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 <React/RCTViewComponentView.h>
12
+ #import <UIKit/UIKit.h>
13
+
14
+ NS_ASSUME_NONNULL_BEGIN
15
+
16
+ /**
17
+ * Hidden host that renders its children in a container attached directly to
18
+ * the window, above every presented sheet.
19
+ */
20
+ @interface TrueSheetOverlayView : RCTViewComponentView
21
+
22
+ /**
23
+ * Re-stacks every overlay container above the window's other subviews.
24
+ * Called when a sheet presents, since presenting adds its transition view on top.
25
+ */
26
+ + (void)bringOverlaysToFrontInWindow:(nullable UIWindow *)window;
27
+
28
+ @end
29
+
30
+ NS_ASSUME_NONNULL_END
31
+
32
+ #endif
@@ -0,0 +1,161 @@
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 "TrueSheetOverlayView.h"
12
+ #import "core/TrueSheetOverlayContainerView.h"
13
+
14
+ #import <react/renderer/components/TrueSheetSpec/EventEmitters.h>
15
+ #import <react/renderer/components/TrueSheetSpec/Props.h>
16
+ #import <react/renderer/components/TrueSheetSpec/RCTComponentViewHelpers.h>
17
+ #import <react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewComponentDescriptor.h>
18
+ #import <react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.h>
19
+
20
+ #import <React/RCTSurfaceTouchHandler.h>
21
+
22
+ using namespace facebook::react;
23
+
24
+ static NSHashTable<TrueSheetOverlayView *> *gOverlayViews;
25
+
26
+ @interface TrueSheetOverlayView () <TrueSheetOverlayContainerViewDelegate>
27
+ @end
28
+
29
+ @implementation TrueSheetOverlayView {
30
+ TrueSheetOverlayContainerView *_containerView;
31
+ // Owns `_containerView` so it has a view controller in its responder chain. The
32
+ // container is a sibling of `rootViewController.view`, and a window's responder chain
33
+ // skips its root view controller — without an owner, `-[UIView reactViewController]`
34
+ // finds nothing and content that installs a view controller cannot attach.
35
+ UIViewController *_containerController;
36
+ RCTSurfaceTouchHandler *_touchHandler;
37
+ TrueSheetOverlayViewShadowNode::ConcreteState::Shared _state;
38
+ CGSize _lastStateSize;
39
+ }
40
+
41
+ + (ComponentDescriptorProvider)componentDescriptorProvider {
42
+ return concreteComponentDescriptorProvider<TrueSheetOverlayViewComponentDescriptor>();
43
+ }
44
+
45
+ + (void)bringOverlaysToFrontInWindow:(nullable UIWindow *)window {
46
+ if (!window)
47
+ return;
48
+
49
+ for (TrueSheetOverlayView *overlay in gOverlayViews) {
50
+ if (overlay->_containerView.superview == window) {
51
+ [window bringSubviewToFront:overlay->_containerView];
52
+ }
53
+ }
54
+ }
55
+
56
+ - (instancetype)initWithFrame:(CGRect)frame {
57
+ if (self = [super initWithFrame:frame]) {
58
+ static const auto defaultProps = std::make_shared<const TrueSheetOverlayViewProps>();
59
+ _props = defaultProps;
60
+
61
+ _containerView = [[TrueSheetOverlayContainerView alloc] initWithFrame:CGRectZero];
62
+ _containerView.delegate = self;
63
+
64
+ // Assigning the container as its view is what puts the controller in the responder
65
+ // chain; the containment below is for lifecycle only.
66
+ _containerController = [UIViewController new];
67
+ _containerController.view = _containerView;
68
+
69
+ _touchHandler = [[RCTSurfaceTouchHandler alloc] init];
70
+ [_touchHandler attachToView:_containerView];
71
+
72
+ _lastStateSize = CGSizeZero;
73
+
74
+ // The host stays in the React tree for layout only — children render in the container
75
+ self.hidden = YES;
76
+
77
+ if (!gOverlayViews) {
78
+ gOverlayViews = [NSHashTable weakObjectsHashTable];
79
+ }
80
+ [gOverlayViews addObject:self];
81
+ }
82
+ return self;
83
+ }
84
+
85
+ - (void)didMoveToWindow {
86
+ [super didMoveToWindow];
87
+
88
+ UIWindow *window = self.window;
89
+ if (window) {
90
+ _containerView.frame = window.bounds;
91
+ [window addSubview:_containerView];
92
+
93
+ UIViewController *root = window.rootViewController;
94
+ if (root != nil && _containerController.parentViewController != root) {
95
+ [root addChildViewController:_containerController];
96
+ [_containerController didMoveToParentViewController:root];
97
+ }
98
+ } else {
99
+ [_containerController willMoveToParentViewController:nil];
100
+ [_containerView removeFromSuperview];
101
+ [_containerController removeFromParentViewController];
102
+ }
103
+ }
104
+
105
+ #pragma mark - State
106
+
107
+ - (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState {
108
+ _state = std::static_pointer_cast<TrueSheetOverlayViewShadowNode::ConcreteState const>(state);
109
+
110
+ if (self.window) {
111
+ [self updateStateWithSize:self.window.bounds.size];
112
+ }
113
+ }
114
+
115
+ - (void)overlayContainerViewDidLayout:(CGSize)size {
116
+ [self updateStateWithSize:size];
117
+ }
118
+
119
+ - (void)updateStateWithSize:(CGSize)size {
120
+ if (!_state)
121
+ return;
122
+
123
+ if (fabs(size.width - _lastStateSize.width) < 0.5 && fabs(size.height - _lastStateSize.height) < 0.5)
124
+ return;
125
+
126
+ _lastStateSize = size;
127
+
128
+ auto stateData = _state->getData();
129
+ stateData.containerWidth = static_cast<float>(size.width);
130
+ stateData.containerHeight = static_cast<float>(size.height);
131
+ _state->updateState(std::move(stateData), EventQueue::UpdateMode::unstable_Immediate);
132
+ }
133
+
134
+ #pragma mark - Child Component Mounting
135
+
136
+ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
137
+ [_containerView insertSubview:childComponentView atIndex:index];
138
+ }
139
+
140
+ - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
141
+ [childComponentView removeFromSuperview];
142
+ }
143
+
144
+ - (void)prepareForRecycle {
145
+ [super prepareForRecycle];
146
+
147
+ [_containerController willMoveToParentViewController:nil];
148
+ [_containerView removeFromSuperview];
149
+ [_containerController removeFromParentViewController];
150
+ _lastStateSize = CGSizeZero;
151
+ _state.reset();
152
+ self.hidden = YES;
153
+ }
154
+
155
+ @end
156
+
157
+ Class<RCTComponentViewProtocol> TrueSheetOverlayViewCls(void) {
158
+ return TrueSheetOverlayView.class;
159
+ }
160
+
161
+ #endif
@@ -13,6 +13,7 @@
13
13
  #import "TrueSheetContentView.h"
14
14
  #import "TrueSheetFooterView.h"
15
15
  #import "TrueSheetModule.h"
16
+ #import "TrueSheetOverlayView.h"
16
17
  #import "TrueSheetViewController.h"
17
18
  #import "core/RNScreensEventObserver.h"
18
19
  #import "events/TrueSheetDragEvents.h"
@@ -53,7 +54,7 @@ using namespace facebook::react;
53
54
  NSInteger _initialDetentIndex;
54
55
  TrueSheetViewInsetAdjustment _insetAdjustment;
55
56
  ScrollableOptions *_scrollableOptions;
56
- BOOL _hasAutoDetent;
57
+ NSInteger _scrollableHandle;
57
58
  BOOL _initialDetentAnimated;
58
59
  BOOL _isSheetUpdatePending;
59
60
  BOOL _pendingLayoutUpdate;
@@ -110,18 +111,72 @@ using namespace facebook::react;
110
111
  return;
111
112
  }
112
113
 
113
- if (_initialDetentIndex >= 0 && !_didInitiallyPresent) {
114
- UIViewController *vc = [self findPresentingViewController];
114
+ [self presentInitialIfNeeded];
115
+ }
116
+
117
+ // JS holds initialDetentIndex back one commit so effect-driven content (e.g. a
118
+ // navigation footer set via setOptions) commits together with it — by the time
119
+ // the prop lands the tree is final and the sheet presents at its final height.
120
+ // Called from didMoveToWindow (re-attach) and finalizeUpdates (the prop
121
+ // arriving after attach, which didMoveToWindow won't re-fire for).
122
+ - (void)presentInitialIfNeeded {
123
+ if (_initialDetentIndex < 0 || _didInitiallyPresent)
124
+ return;
115
125
 
116
- // Only present if the view controller is in the same window and not being dismissed
117
- if (vc && vc.view.window == self.window && !_controller.isBeingDismissed) {
118
- _didInitiallyPresent = YES;
119
- [self presentAtIndex:_initialDetentIndex animated:_initialDetentAnimated completion:nil];
126
+ UIViewController *vc = [self findPresentingViewController];
127
+
128
+ // Only present if the view controller is in the same window
129
+ if (!vc || vc.view.window != self.window) {
130
+ // Animate next time when sheet finally moves to the correct window
131
+ _initialDetentAnimated = YES;
132
+ return;
133
+ }
134
+
135
+ // A sheet still animating out of the presenter (e.g. our own controller,
136
+ // recycled mid-dismissal) blocks presentation — retry once its transition ends.
137
+ UIViewController *dismissing = vc.presentedViewController;
138
+ if (dismissing && dismissing.isBeingDismissed) {
139
+ _initialDetentAnimated = YES;
140
+
141
+ __weak __typeof(self) weakSelf = self;
142
+ void (^retry)(void) = ^{
143
+ dispatch_async(dispatch_get_main_queue(), ^{
144
+ [weakSelf presentInitialIfNeeded];
145
+ });
146
+ };
147
+
148
+ id<UIViewControllerTransitionCoordinator> coordinator = dismissing.transitionCoordinator;
149
+ if (coordinator) {
150
+ [coordinator animateAlongsideTransition:nil
151
+ completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
152
+ retry();
153
+ }];
120
154
  } else {
121
- // Animate next time when sheet finally moves to the correct window
122
- _initialDetentAnimated = YES;
155
+ retry();
123
156
  }
157
+ return;
124
158
  }
159
+
160
+ _didInitiallyPresent = YES;
161
+
162
+ // Deferred a runloop turn so sibling mounts from the current transaction
163
+ // (e.g. a footer committed alongside the prop) land before measuring.
164
+ __weak __typeof(self) weakSelf = self;
165
+ dispatch_async(dispatch_get_main_queue(), ^{
166
+ __typeof(self) strongSelf = weakSelf;
167
+ if (!strongSelf || strongSelf->_controller.isPresented || strongSelf->_controller.isBeingPresented)
168
+ return;
169
+
170
+ [strongSelf presentAtIndex:strongSelf->_initialDetentIndex
171
+ animated:strongSelf->_initialDetentAnimated
172
+ completion:^(BOOL success, NSError *_Nullable error) {
173
+ // Present can fail if the view detached during this turn —
174
+ // reset so the next attach/prop update retries.
175
+ if (!success) {
176
+ strongSelf->_didInitiallyPresent = NO;
177
+ }
178
+ }];
179
+ });
125
180
  }
126
181
 
127
182
  - (void)dealloc {
@@ -146,8 +201,6 @@ using namespace facebook::react;
146
201
 
147
202
  [_snapshotView removeFromSuperview];
148
203
  _snapshotView = nil;
149
-
150
- [TrueSheetModule unregisterViewWithTag:@(self.tag)];
151
204
  }
152
205
 
153
206
  #pragma mark - RCTComponentViewProtocol
@@ -163,12 +216,8 @@ using namespace facebook::react;
163
216
 
164
217
  // Detents (-1 represents "auto")
165
218
  NSMutableArray *detents = [NSMutableArray new];
166
- _hasAutoDetent = NO;
167
219
  for (const auto &detent : newProps.detents) {
168
220
  [detents addObject:@(detent)];
169
- if (detent == -1) {
170
- _hasAutoDetent = YES;
171
- }
172
221
  }
173
222
 
174
223
  if (oldProps) {
@@ -248,17 +297,23 @@ using namespace facebook::react;
248
297
  _initialDetentIndex = newProps.initialDetentIndex;
249
298
  _initialDetentAnimated = newProps.initialDetentAnimated;
250
299
 
300
+ _scrollableHandle = newProps.scrollableHandle;
301
+
251
302
  const auto &scrollableOpts = newProps.scrollableOptions;
252
303
  BOOL scrollingExpandsSheet = scrollableOpts.scrollingExpandsSheet;
304
+ BOOL contentInsetAdjustment = scrollableOpts.contentInsetAdjustmentBehavior;
253
305
  auto topEdgeEffect = scrollableOpts.topScrollEdgeEffect;
254
306
  auto bottomEdgeEffect = scrollableOpts.bottomScrollEdgeEffect;
255
- BOOL hasScrollableOptions = scrollableOpts.keyboardScrollOffset > 0 || !scrollingExpandsSheet ||
307
+ BOOL hasScrollableOptions = scrollableOpts.keyboardScrollOffset > 0 || scrollableOpts.keyboardOffset != 0 ||
308
+ !scrollingExpandsSheet || !contentInsetAdjustment ||
256
309
  topEdgeEffect != TrueSheetViewTopScrollEdgeEffect::Hidden ||
257
310
  bottomEdgeEffect != TrueSheetViewBottomScrollEdgeEffect::Hidden;
258
311
 
259
312
  if (hasScrollableOptions) {
260
313
  ScrollableOptions *options = [[ScrollableOptions alloc] init];
314
+ options.contentInsetAdjustment = contentInsetAdjustment;
261
315
  options.keyboardScrollOffset = scrollableOpts.keyboardScrollOffset;
316
+ options.keyboardOffset = scrollableOpts.keyboardOffset;
262
317
  options.scrollingExpandsSheet = scrollingExpandsSheet;
263
318
  options.topScrollEdgeEffect = topEdgeEffect;
264
319
  options.bottomScrollEdgeEffect = bottomEdgeEffect;
@@ -342,13 +397,47 @@ using namespace facebook::react;
342
397
  _pendingPropsUpdate = YES;
343
398
  } else if (_initialDetentIndex >= 0) {
344
399
  _pendingLayoutUpdate = NO;
400
+ if (self.window) {
401
+ [self presentInitialIfNeeded];
402
+ }
345
403
  }
346
404
  }
347
405
 
348
406
  - (void)prepareForRecycle {
349
407
  [super prepareForRecycle];
350
408
 
351
- [TrueSheetModule unregisterViewWithTag:@(self.tag)];
409
+ // Pooled mid-dismissal: the next mount can reuse this host — and its
410
+ // controller — while the previous sheet is still animating out. Detach until
411
+ // that transition ends so its tail (didBlur, didDismiss, position) can't reach
412
+ // the new mount, then hand the controller back and let auto-present retry.
413
+ if (_controller.isBeingDismissed) {
414
+ _controller.delegate = nil;
415
+
416
+ __weak __typeof(self) weakSelf = self;
417
+ void (^reattach)(void) = ^{
418
+ dispatch_async(dispatch_get_main_queue(), ^{
419
+ __typeof(self) strongSelf = weakSelf;
420
+ if (!strongSelf)
421
+ return;
422
+
423
+ strongSelf->_controller.delegate = strongSelf;
424
+ strongSelf->_controller.activeDetentIndex = -1;
425
+ if (strongSelf.window) {
426
+ [strongSelf presentInitialIfNeeded];
427
+ }
428
+ });
429
+ };
430
+
431
+ id<UIViewControllerTransitionCoordinator> coordinator = _controller.transitionCoordinator;
432
+ if (coordinator) {
433
+ [coordinator animateAlongsideTransition:nil
434
+ completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
435
+ reattach();
436
+ }];
437
+ } else {
438
+ reattach();
439
+ }
440
+ }
352
441
 
353
442
  _lastStateSize = CGSizeZero;
354
443
  _didInitiallyPresent = NO;
@@ -389,7 +478,6 @@ using namespace facebook::react;
389
478
  [_touchHandler attachToView:_containerView];
390
479
  [_controller.view addSubview:_containerView];
391
480
  [_controller.view bringSubviewToFront:_containerView];
392
- _containerView.accessibilityViewIsModal = YES;
393
481
  _controller.accessibilityContentView = _containerView;
394
482
  [_controller setupAccessibilityContainer];
395
483
 
@@ -403,9 +491,8 @@ using namespace facebook::react;
403
491
  _controller.headerHeight = @(headerHeight);
404
492
  }
405
493
 
406
- CGFloat footerHeight = [_containerView footerHeight];
407
- if (footerHeight > 0) {
408
- _controller.footerHeight = @(footerHeight);
494
+ if ([_containerView footerHeight] > 0) {
495
+ [self syncFooterMetrics];
409
496
  }
410
497
 
411
498
  CGFloat peekContentHeight = [_containerView peekContentHeight];
@@ -471,7 +558,7 @@ using namespace facebook::react;
471
558
  [_controller setupSheetSizing];
472
559
  [_controller setupSheetProps];
473
560
 
474
- // Pin before measuring so a first-time pin doesn't shift sizes mid-animation.
561
+ // Detect before measuring so a first-time detection doesn't shift sizes mid-animation.
475
562
  [self setupScrollable];
476
563
 
477
564
  // Measure synchronously so the presentation starts at the right height —
@@ -480,24 +567,41 @@ using namespace facebook::react;
480
567
  if (_containerView) {
481
568
  _controller.contentHeight = @([_containerView contentHeight]);
482
569
  _controller.headerHeight = @([_containerView headerHeight]);
483
- _controller.footerHeight = @([_containerView footerHeight]);
570
+ [self syncFooterMetrics];
484
571
  _controller.peekContentHeight = @([_containerView peekContentHeight]);
485
572
  }
486
573
  _pendingSizeChange = NO;
487
574
 
575
+ [self refreshFooterBottomInset];
488
576
  [_controller setupSheetDetents];
489
577
  [_controller setupActiveDetentWithIndex:index];
490
578
 
491
579
  [_screensEventObserver capturePresenterScreenFromView:self];
492
580
  [_screensEventObserver startObservingWithState:_state.get()->getData()];
493
581
 
582
+ UIWindow *window = presentingViewController.view.window;
494
583
  [presentingViewController presentViewController:_controller
495
584
  animated:animated
496
585
  completion:^{
586
+ [TrueSheetOverlayView bringOverlaysToFrontInWindow:window];
497
587
  if (completion) {
498
588
  completion(YES, nil);
499
589
  }
500
590
  }];
591
+
592
+ // Presenting adds the sheet's transition view on top of the window once the
593
+ // transition starts — re-front overlays alongside it so the first frame is
594
+ // already ordered (and again on completion in case the transition was queued)
595
+ id<UIViewControllerTransitionCoordinator> coordinator = _controller.transitionCoordinator;
596
+ if (coordinator) {
597
+ [coordinator
598
+ animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
599
+ [TrueSheetOverlayView bringOverlaysToFrontInWindow:window];
600
+ }
601
+ completion:nil];
602
+ } else {
603
+ [TrueSheetOverlayView bringOverlaysToFrontInWindow:window];
604
+ }
501
605
  }
502
606
 
503
607
  - (void)resizeToIndex:(NSInteger)index completion:(nullable TrueSheetCompletionBlock)completion {
@@ -583,6 +687,10 @@ using namespace facebook::react;
583
687
  * Debounced sheet update to handle rapid content/header size changes.
584
688
  */
585
689
  - (void)setupSheetDetentsForSizeChange {
690
+ // Keep the footer's absorbed inset in sync with the latest measured heights
691
+ // even before presenting, so the sheet presents at its final size.
692
+ [self refreshFooterBottomInset];
693
+
586
694
  // Retargeting the in-flight presentation makes UIKit snap the whole stack
587
695
  // (including the sheet behind) instead of animating. Defer to didPresent —
588
696
  // presentAtIndex measured synchronously, so this is usually a no-op.
@@ -610,6 +718,7 @@ using namespace facebook::react;
610
718
  // Refresh here (not just on peek size events) since the peek's offset
611
719
  // within the content can change without its own size changing.
612
720
  self->_controller.peekContentHeight = @([self->_containerView peekContentHeight]);
721
+ [self refreshFooterBottomInset];
613
722
  [self->_controller setupSheetDetentsForSizeChange];
614
723
  });
615
724
  }
@@ -625,7 +734,7 @@ using namespace facebook::react;
625
734
  }
626
735
 
627
736
  - (void)containerViewFooterDidChangeSize:(CGSize)newSize {
628
- _controller.footerHeight = @(newSize.height);
737
+ [self syncFooterMetrics];
629
738
  [self setupSheetDetentsForSizeChange];
630
739
  [_controller setupAccessibilityContainer];
631
740
  }
@@ -634,7 +743,7 @@ using namespace facebook::react;
634
743
  [self setupSheetDetentsForSizeChange];
635
744
  }
636
745
 
637
- // When the ScrollView changes (e.g. conditional remount), re-pin the new ScrollView.
746
+ // When the ScrollView changes (e.g. conditional remount), re-detect the new ScrollView.
638
747
  - (void)containerViewScrollViewDidChange {
639
748
  [self setupScrollable];
640
749
  }
@@ -687,6 +796,10 @@ using namespace facebook::react;
687
796
  }
688
797
  }
689
798
 
799
+ - (void)viewControllerDidAttemptDismiss {
800
+ [TrueSheetLifecycleEvents emitDismissAttempt:_eventEmitter];
801
+ }
802
+
690
803
  - (void)viewControllerDidDismiss {
691
804
  [_containerView cleanupKeyboardObserver];
692
805
  if (!_dismissedByNavigation) {
@@ -716,6 +829,10 @@ using namespace facebook::react;
716
829
  [self updateStateWithSize:size];
717
830
  }
718
831
 
832
+ - (void)viewControllerSafeAreaInsetsDidChange {
833
+ [self refreshFooterBottomInset];
834
+ }
835
+
719
836
  - (void)viewControllerWillFocus {
720
837
  [TrueSheetFocusEvents emitWillFocus:_eventEmitter];
721
838
  }
@@ -781,16 +898,40 @@ using namespace facebook::react;
781
898
  if (!_containerView)
782
899
  return;
783
900
 
784
- _containerView.insetAdjustment = _insetAdjustment;
785
901
  _containerView.scrollableOptions = _scrollableOptions;
786
- _containerView.hasAutoDetent = _hasAutoDetent;
787
- _containerView.footerBottomInset = _controller.footerBottomInset;
902
+ _containerView.scrollableHandle = _scrollableHandle;
903
+ _containerView.contentInsetAdjustment = (_scrollableOptions ? _scrollableOptions.contentInsetAdjustment : YES) &&
904
+ _insetAdjustment == TrueSheetViewInsetAdjustment::Automatic;
905
+ [self refreshFooterBottomInset];
788
906
  [_containerView setupScrollable];
907
+ }
908
+
909
+ // footerHeight and appliedFooterBottomInset must stay paired — detent math
910
+ // subtracts exactly the inset baked into the measured footer height (see
911
+ // autoDetentBottomInset/peekDetentBottomInset).
912
+ - (void)syncFooterMetrics {
913
+ _controller.footerHeight = @([_containerView footerHeight]);
914
+ _controller.appliedFooterBottomInset = [_containerView footerAppliedBottomInset];
915
+ }
916
+
917
+ // Recomputes the inset the footer absorbs and pushes it down. The inset
918
+ // depends on measured heights and detents (see maxNaturalDetentHeight), so
919
+ // this runs on size and detent changes — not just prop updates.
920
+ - (void)refreshFooterBottomInset {
921
+ if (!_containerView)
922
+ return;
923
+
924
+ // appliedFooterBottomInset is NOT set here — it tracks what's actually
925
+ // baked into the footer's measured height (reported with its layout), not
926
+ // what was just pushed. Setting it at push time desyncs the pair and lets
927
+ // a seeded inset read as natural content height (see maxNaturalDetentHeight).
928
+ CGFloat inset = _controller.footerBottomInset;
929
+ _containerView.footerBottomInset = inset;
789
930
 
790
931
  // Publish the inset so a footer set later (e.g. navigation setOptions) is
791
932
  // padded on its very first layout instead of resizing the sheet twice.
792
933
  if (_insetAdjustment == TrueSheetViewInsetAdjustment::Automatic) {
793
- TrueSheetInsets::setBottomSafeArea(_controller.footerBottomInset);
934
+ TrueSheetInsets::setBottomSafeArea(inset);
794
935
  }
795
936
  }
796
937
 
@@ -801,6 +942,7 @@ using namespace facebook::react;
801
942
  if (_pendingDetents) {
802
943
  _controller.detents = _pendingDetents;
803
944
  _pendingDetents = nil;
945
+ [self refreshFooterBottomInset];
804
946
  }
805
947
 
806
948
  UIView *presenterView = _controller.presentingViewController.view;
@@ -26,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN
26
26
  - (void)viewControllerDidPresentAtIndex:(NSInteger)index position:(CGFloat)position detent:(CGFloat)detent;
27
27
  - (void)viewControllerWillDismiss;
28
28
  - (void)viewControllerDidDismiss;
29
+ - (void)viewControllerDidAttemptDismiss;
29
30
  - (void)viewControllerDidChangeDetent:(NSInteger)index position:(CGFloat)position detent:(CGFloat)detent;
30
31
  - (void)viewControllerDidDrag:(UIGestureRecognizerState)state
31
32
  index:(NSInteger)index
@@ -36,6 +37,7 @@ NS_ASSUME_NONNULL_BEGIN
36
37
  detent:(CGFloat)detent
37
38
  realtime:(BOOL)realtime;
38
39
  - (void)viewControllerDidChangeSize:(CGSize)size;
40
+ - (void)viewControllerSafeAreaInsetsDidChange;
39
41
  - (void)viewControllerWillFocus;
40
42
  - (void)viewControllerDidFocus;
41
43
  - (void)viewControllerWillBlur;
@@ -99,6 +101,14 @@ NS_ASSUME_NONNULL_BEGIN
99
101
  */
100
102
  @property (nonatomic, readonly) CGFloat footerBottomInset;
101
103
 
104
+ /**
105
+ * The inset currently baked into the footer's measured height, reported
106
+ * alongside its layout (see TrueSheetFooterView.appliedBottomInset). Detent
107
+ * resolution subtracts this exact value so the baked inset is never
108
+ * double-counted.
109
+ */
110
+ @property (nonatomic, assign) CGFloat appliedFooterBottomInset;
111
+
102
112
  - (void)setupAccessibilityContainer;
103
113
  - (void)applyActiveDetent;
104
114
  - (void)setupActiveDetentWithIndex:(NSInteger)index;