@lodev09/react-native-true-sheet 4.0.0-beta.2 → 4.0.0-beta.20

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 (203) 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 +50 -16
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +302 -184
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +5 -1
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +21 -110
  7. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +116 -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 +119 -50
  12. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +360 -163
  13. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +54 -12
  14. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +22 -9
  15. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +84 -5
  16. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +104 -39
  17. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +3 -0
  18. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetOverlayRootView.kt +137 -0
  19. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +15 -0
  20. package/android/src/main/java/com/lodev09/truesheet/utils/KeyboardUtils.kt +15 -2
  21. package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +0 -12
  22. package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +19 -7
  23. package/android/src/main/jni/TrueSheetSpec.h +1 -0
  24. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +1 -9
  25. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +110 -30
  26. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +30 -7
  27. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +2 -2
  28. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +9 -8
  29. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h +3 -5
  30. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +9 -3
  31. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +3 -6
  32. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +1 -1
  33. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +4 -6
  34. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.cpp +1 -1
  35. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.h +1 -1
  36. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetLayoutUtils.h +43 -0
  37. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewComponentDescriptor.h +21 -0
  38. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.cpp +17 -0
  39. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.h +33 -0
  40. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h +3 -5
  41. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +7 -40
  42. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.h +3 -7
  43. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.cpp +3 -5
  44. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.h +4 -5
  45. package/ios/TrueSheetContainerView.h +27 -7
  46. package/ios/TrueSheetContainerView.mm +31 -11
  47. package/ios/TrueSheetContentView.h +43 -15
  48. package/ios/TrueSheetContentView.mm +211 -189
  49. package/ios/TrueSheetFooterView.h +6 -0
  50. package/ios/TrueSheetFooterView.mm +93 -17
  51. package/ios/TrueSheetModule.h +2 -7
  52. package/ios/TrueSheetModule.mm +7 -16
  53. package/ios/TrueSheetOverlayView.h +32 -0
  54. package/ios/TrueSheetOverlayView.mm +161 -0
  55. package/ios/TrueSheetView.mm +212 -55
  56. package/ios/TrueSheetViewController.h +31 -3
  57. package/ios/TrueSheetViewController.mm +393 -94
  58. package/ios/core/TrueSheetBlurView.h +0 -2
  59. package/ios/core/TrueSheetBlurView.mm +3 -36
  60. package/ios/core/TrueSheetDetentCalculator.h +7 -1
  61. package/ios/core/TrueSheetDetentCalculator.mm +27 -4
  62. package/ios/core/TrueSheetGrabberView.mm +13 -3
  63. package/ios/core/TrueSheetKeyboardObserver.mm +2 -0
  64. package/ios/core/TrueSheetOverlayContainerView.h +31 -0
  65. package/ios/core/TrueSheetOverlayContainerView.mm +35 -0
  66. package/ios/events/TrueSheetLifecycleEvents.h +2 -0
  67. package/ios/events/TrueSheetLifecycleEvents.mm +8 -0
  68. package/ios/tvos/TrueSheetStubs.mm +12 -0
  69. package/ios/utils/UIView+ScrollEdgeInteraction.h +1 -0
  70. package/ios/utils/UIView+ScrollEdgeInteraction.mm +24 -2
  71. package/lib/module/TrueSheet.js +173 -39
  72. package/lib/module/TrueSheet.js.map +1 -1
  73. package/lib/module/TrueSheet.web.js +218 -243
  74. package/lib/module/TrueSheet.web.js.map +1 -1
  75. package/lib/module/TrueSheetBackground.js +15 -0
  76. package/lib/module/TrueSheetBackground.js.map +1 -0
  77. package/lib/module/TrueSheetOverlay.js +30 -0
  78. package/lib/module/TrueSheetOverlay.js.map +1 -0
  79. package/lib/module/TrueSheetOverlay.web.js +35 -0
  80. package/lib/module/TrueSheetOverlay.web.js.map +1 -0
  81. package/lib/module/TrueSheetPeek.web.js +20 -15
  82. package/lib/module/TrueSheetPeek.web.js.map +1 -1
  83. package/lib/module/detentBackgrounds.js +21 -0
  84. package/lib/module/detentBackgrounds.js.map +1 -0
  85. package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  86. package/lib/module/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  87. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +14 -10
  88. package/lib/module/index.js +1 -0
  89. package/lib/module/index.js.map +1 -1
  90. package/lib/module/mocks/index.js +18 -2
  91. package/lib/module/mocks/index.js.map +1 -1
  92. package/lib/module/mocks/navigationExpoRouter.js.map +1 -1
  93. package/lib/module/navigation/base-types.js +2 -0
  94. package/lib/module/navigation/base-types.js.map +1 -0
  95. package/lib/module/navigation/createTrueSheetRouter.js +0 -9
  96. package/lib/module/navigation/createTrueSheetRouter.js.map +1 -1
  97. package/lib/module/navigation/expo-router/base-types.js +2 -0
  98. package/lib/module/navigation/expo-router/base-types.js.map +1 -0
  99. package/lib/module/navigation/expo-router/index.js +10 -7
  100. package/lib/module/navigation/expo-router/index.js.map +1 -1
  101. package/lib/module/navigation/expo-router/types.js +4 -0
  102. package/lib/module/navigation/expo-router/types.js.map +1 -0
  103. package/lib/module/navigation/index.js.map +1 -1
  104. package/lib/module/navigation/screen/useSheetScreenState.js +1 -0
  105. package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -1
  106. package/lib/module/web/constants.js +1 -1
  107. package/lib/module/web/constants.js.map +1 -1
  108. package/lib/module/web/layout.js +168 -0
  109. package/lib/module/web/layout.js.map +1 -0
  110. package/lib/module/web/vaul/constants.js +4 -0
  111. package/lib/module/web/vaul/constants.js.map +1 -1
  112. package/lib/module/web/vaul/context.js.map +1 -1
  113. package/lib/module/web/vaul/helpers.js +1 -0
  114. package/lib/module/web/vaul/helpers.js.map +1 -1
  115. package/lib/module/web/vaul/index.js +44 -6
  116. package/lib/module/web/vaul/index.js.map +1 -1
  117. package/lib/module/web/vaul/use-snap-points.js +6 -8
  118. package/lib/module/web/vaul/use-snap-points.js.map +1 -1
  119. package/lib/typescript/src/TrueSheet.d.ts +6 -0
  120. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  121. package/lib/typescript/src/TrueSheet.types.d.ts +135 -34
  122. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  123. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  124. package/lib/typescript/src/TrueSheetBackground.d.ts +3 -0
  125. package/lib/typescript/src/TrueSheetBackground.d.ts.map +1 -0
  126. package/lib/typescript/src/TrueSheetOverlay.d.ts +8 -0
  127. package/lib/typescript/src/TrueSheetOverlay.d.ts.map +1 -0
  128. package/lib/typescript/src/TrueSheetOverlay.web.d.ts +8 -0
  129. package/lib/typescript/src/TrueSheetOverlay.web.d.ts.map +1 -0
  130. package/lib/typescript/src/TrueSheetPeek.web.d.ts +8 -4
  131. package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
  132. package/lib/typescript/src/detentBackgrounds.d.ts +10 -0
  133. package/lib/typescript/src/detentBackgrounds.d.ts.map +1 -0
  134. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts +6 -0
  135. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts.map +1 -0
  136. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +11 -7
  137. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  138. package/lib/typescript/src/index.d.ts +1 -0
  139. package/lib/typescript/src/index.d.ts.map +1 -1
  140. package/lib/typescript/src/mocks/index.d.ts +4 -0
  141. package/lib/typescript/src/mocks/index.d.ts.map +1 -1
  142. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +3 -3
  143. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -1
  144. package/lib/typescript/src/navigation/base-types.d.ts +9 -0
  145. package/lib/typescript/src/navigation/base-types.d.ts.map +1 -0
  146. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +2 -13
  147. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -1
  148. package/lib/typescript/src/navigation/expo-router/base-types.d.ts +8 -0
  149. package/lib/typescript/src/navigation/expo-router/base-types.d.ts.map +1 -0
  150. package/lib/typescript/src/navigation/expo-router/index.d.ts +8 -6
  151. package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -1
  152. package/lib/typescript/src/navigation/expo-router/types.d.ts +44 -0
  153. package/lib/typescript/src/navigation/expo-router/types.d.ts.map +1 -0
  154. package/lib/typescript/src/navigation/index.d.ts +3 -4
  155. package/lib/typescript/src/navigation/index.d.ts.map +1 -1
  156. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +2 -1
  157. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -1
  158. package/lib/typescript/src/navigation/types.d.ts +41 -5
  159. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  160. package/lib/typescript/src/web/constants.d.ts +1 -1
  161. package/lib/typescript/src/web/constants.d.ts.map +1 -1
  162. package/lib/typescript/src/web/layout.d.ts +19 -0
  163. package/lib/typescript/src/web/layout.d.ts.map +1 -0
  164. package/lib/typescript/src/web/vaul/constants.d.ts +1 -0
  165. package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -1
  166. package/lib/typescript/src/web/vaul/context.d.ts +1 -0
  167. package/lib/typescript/src/web/vaul/context.d.ts.map +1 -1
  168. package/lib/typescript/src/web/vaul/helpers.d.ts +1 -0
  169. package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
  170. package/lib/typescript/src/web/vaul/index.d.ts +7 -1
  171. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
  172. package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
  173. package/package.json +17 -8
  174. package/react-native.config.js +1 -0
  175. package/src/TrueSheet.tsx +206 -48
  176. package/src/TrueSheet.types.ts +142 -35
  177. package/src/TrueSheet.web.tsx +270 -290
  178. package/src/TrueSheetBackground.tsx +17 -0
  179. package/src/TrueSheetOverlay.tsx +24 -0
  180. package/src/TrueSheetOverlay.web.tsx +33 -0
  181. package/src/TrueSheetPeek.web.tsx +25 -23
  182. package/src/detentBackgrounds.ts +24 -0
  183. package/src/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  184. package/src/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  185. package/src/fabric/TrueSheetViewNativeComponent.ts +14 -10
  186. package/src/index.ts +1 -0
  187. package/src/mocks/index.ts +17 -2
  188. package/src/mocks/navigationExpoRouter.ts +6 -5
  189. package/src/navigation/base-types.ts +20 -0
  190. package/src/navigation/createTrueSheetRouter.ts +5 -33
  191. package/src/navigation/expo-router/base-types.ts +18 -0
  192. package/src/navigation/expo-router/index.ts +28 -30
  193. package/src/navigation/expo-router/types.ts +116 -0
  194. package/src/navigation/index.ts +6 -7
  195. package/src/navigation/screen/useSheetScreenState.ts +2 -0
  196. package/src/navigation/types.ts +75 -22
  197. package/src/web/constants.ts +1 -1
  198. package/src/web/layout.ts +196 -0
  199. package/src/web/vaul/constants.ts +4 -0
  200. package/src/web/vaul/context.ts +1 -0
  201. package/src/web/vaul/helpers.ts +5 -0
  202. package/src/web/vaul/index.tsx +67 -5
  203. package/src/web/vaul/use-snap-points.ts +6 -14
@@ -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;
125
+
126
+ UIViewController *vc = [self findPresentingViewController];
115
127
 
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];
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) {
@@ -187,14 +236,29 @@ using namespace facebook::react;
187
236
  // Background color
188
237
  _controller.backgroundColor = RCTUIColorFromSharedColor(newProps.backgroundColor);
189
238
 
239
+ NSMutableArray<NSDictionary *> *detentBackgrounds = [NSMutableArray new];
240
+ ContextContainer contextContainer;
241
+ PropsParserContext parserContext{-1, contextContainer};
242
+ for (const auto &entry : newProps.detentBackgrounds) {
243
+ NSMutableDictionary *background = [NSMutableDictionary new];
244
+ UIColor *color = RCTUIColorFromSharedColor(entry.color);
245
+ if (color) {
246
+ background[@"color"] = color;
247
+ }
248
+ if (!entry.blur.empty()) {
249
+ auto blur = TrueSheetViewBackgroundBlur::None;
250
+ fromRawValue(parserContext, RawValue(entry.blur), blur);
251
+ background[@"blur"] = @(static_cast<int>(blur));
252
+ }
253
+ [detentBackgrounds addObject:background];
254
+ }
255
+ if (![_controller.detentBackgrounds isEqualToArray:detentBackgrounds]) {
256
+ _controller.detentBackgrounds = detentBackgrounds;
257
+ }
258
+
190
259
  // Blur tint
191
260
  _controller.backgroundBlur = newProps.backgroundBlur;
192
261
 
193
- // Blur options
194
- const auto &blurOpts = newProps.blurOptions;
195
- _controller.blurIntensity = blurOpts.intensity >= 0 ? @(blurOpts.intensity) : nil;
196
- _controller.blurInteraction = blurOpts.interaction;
197
-
198
262
  // Corner radius
199
263
  _controller.cornerRadius = newProps.cornerRadius < 0 ? nil : @(newProps.cornerRadius);
200
264
 
@@ -204,8 +268,7 @@ using namespace facebook::react;
204
268
  // Content width
205
269
  _controller.maxContentWidth = newProps.maxContentWidth != 0.0 ? @(newProps.maxContentWidth) : nil;
206
270
 
207
- // Anchor
208
- _controller.anchor = newProps.anchor;
271
+ _controller.placement = newProps.placement;
209
272
 
210
273
  _controller.grabber = newProps.grabber;
211
274
 
@@ -248,33 +311,38 @@ using namespace facebook::react;
248
311
  _initialDetentIndex = newProps.initialDetentIndex;
249
312
  _initialDetentAnimated = newProps.initialDetentAnimated;
250
313
 
251
- const auto &scrollableOpts = newProps.scrollableOptions;
252
- BOOL scrollingExpandsSheet = scrollableOpts.scrollingExpandsSheet;
253
- auto topEdgeEffect = scrollableOpts.topScrollEdgeEffect;
254
- auto bottomEdgeEffect = scrollableOpts.bottomScrollEdgeEffect;
255
- BOOL hasScrollableOptions = scrollableOpts.keyboardScrollOffset > 0 || !scrollingExpandsSheet ||
256
- topEdgeEffect != TrueSheetViewTopScrollEdgeEffect::Hidden ||
257
- bottomEdgeEffect != TrueSheetViewBottomScrollEdgeEffect::Hidden;
258
-
259
- if (hasScrollableOptions) {
260
- ScrollableOptions *options = [[ScrollableOptions alloc] init];
261
- options.keyboardScrollOffset = scrollableOpts.keyboardScrollOffset;
262
- options.scrollingExpandsSheet = scrollingExpandsSheet;
263
- options.topScrollEdgeEffect = topEdgeEffect;
264
- options.bottomScrollEdgeEffect = bottomEdgeEffect;
265
- _scrollableOptions = options;
266
- } else {
267
- _scrollableOptions = nil;
268
- }
314
+ _scrollableHandle = newProps.scrollableHandle;
269
315
 
270
- _controller.scrollingExpandsSheet = scrollingExpandsSheet;
316
+ const auto &scrollableOpts = newProps.scrollableOptions;
317
+ ScrollableOptions *options = [[ScrollableOptions alloc] init];
318
+ options.contentInsetAdjustment = scrollableOpts.contentInsetAdjustment;
319
+ options.keyboardScrollOffset = scrollableOpts.keyboardScrollOffset;
320
+ options.keyboardOffset = scrollableOpts.keyboardOffset;
321
+ options.scrollingExpandsSheet = scrollableOpts.scrollingExpandsSheet;
322
+ options.topScrollEdgeEffect = scrollableOpts.topScrollEdgeEffect;
323
+ options.bottomScrollEdgeEffect = scrollableOpts.bottomScrollEdgeEffect;
324
+
325
+ // nil while every option is at its default
326
+ BOOL hasScrollableOptions = options.keyboardScrollOffset > 0 || options.keyboardOffset != 0 ||
327
+ !options.scrollingExpandsSheet ||
328
+ options.contentInsetAdjustment != TrueSheetViewContentInsetAdjustment::Automatic ||
329
+ options.topScrollEdgeEffect != TrueSheetViewTopScrollEdgeEffect::Hidden ||
330
+ options.bottomScrollEdgeEffect != TrueSheetViewBottomScrollEdgeEffect::Hidden;
331
+ _scrollableOptions = hasScrollableOptions ? options : nil;
332
+
333
+ _controller.scrollingExpandsSheet = options.scrollingExpandsSheet;
271
334
 
272
335
  _controller.absoluteHeader = newProps.headerOptions.position == TrueSheetViewPosition::Absolute;
273
336
  _controller.absoluteFooter = newProps.footerOptions.footerPosition == TrueSheetViewFooterPosition::Absolute;
337
+ _controller.footerInsetAdjustment =
338
+ options.footerInsetAdjustment && _controller.absoluteFooter && _scrollableHandle > 0;
274
339
 
275
340
  CGFloat footerKeyboardOffset = newProps.footerOptions.keyboardOffset;
276
- if (_controller.footerKeyboardOffset != footerKeyboardOffset) {
341
+ BOOL footerAvoidsKeyboard = newProps.footerOptions.avoidKeyboard;
342
+ if (_controller.footerKeyboardOffset != footerKeyboardOffset ||
343
+ _controller.footerAvoidsKeyboard != footerAvoidsKeyboard) {
277
344
  _controller.footerKeyboardOffset = footerKeyboardOffset;
345
+ _controller.footerAvoidsKeyboard = footerAvoidsKeyboard;
278
346
  [_containerView updateFooterKeyboardOffset];
279
347
  }
280
348
 
@@ -342,13 +410,47 @@ using namespace facebook::react;
342
410
  _pendingPropsUpdate = YES;
343
411
  } else if (_initialDetentIndex >= 0) {
344
412
  _pendingLayoutUpdate = NO;
413
+ if (self.window) {
414
+ [self presentInitialIfNeeded];
415
+ }
345
416
  }
346
417
  }
347
418
 
348
419
  - (void)prepareForRecycle {
349
420
  [super prepareForRecycle];
350
421
 
351
- [TrueSheetModule unregisterViewWithTag:@(self.tag)];
422
+ // Pooled mid-dismissal: the next mount can reuse this host — and its
423
+ // controller — while the previous sheet is still animating out. Detach until
424
+ // that transition ends so its tail (didBlur, didDismiss, position) can't reach
425
+ // the new mount, then hand the controller back and let auto-present retry.
426
+ if (_controller.isBeingDismissed) {
427
+ _controller.delegate = nil;
428
+
429
+ __weak __typeof(self) weakSelf = self;
430
+ void (^reattach)(void) = ^{
431
+ dispatch_async(dispatch_get_main_queue(), ^{
432
+ __typeof(self) strongSelf = weakSelf;
433
+ if (!strongSelf)
434
+ return;
435
+
436
+ strongSelf->_controller.delegate = strongSelf;
437
+ strongSelf->_controller.activeDetentIndex = -1;
438
+ if (strongSelf.window) {
439
+ [strongSelf presentInitialIfNeeded];
440
+ }
441
+ });
442
+ };
443
+
444
+ id<UIViewControllerTransitionCoordinator> coordinator = _controller.transitionCoordinator;
445
+ if (coordinator) {
446
+ [coordinator animateAlongsideTransition:nil
447
+ completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
448
+ reattach();
449
+ }];
450
+ } else {
451
+ reattach();
452
+ }
453
+ }
352
454
 
353
455
  _lastStateSize = CGSizeZero;
354
456
  _didInitiallyPresent = NO;
@@ -389,7 +491,6 @@ using namespace facebook::react;
389
491
  [_touchHandler attachToView:_containerView];
390
492
  [_controller.view addSubview:_containerView];
391
493
  [_controller.view bringSubviewToFront:_containerView];
392
- _containerView.accessibilityViewIsModal = YES;
393
494
  _controller.accessibilityContentView = _containerView;
394
495
  [_controller setupAccessibilityContainer];
395
496
 
@@ -403,9 +504,8 @@ using namespace facebook::react;
403
504
  _controller.headerHeight = @(headerHeight);
404
505
  }
405
506
 
406
- CGFloat footerHeight = [_containerView footerHeight];
407
- if (footerHeight > 0) {
408
- _controller.footerHeight = @(footerHeight);
507
+ if ([_containerView footerHeight] > 0) {
508
+ [self syncFooterMetrics];
409
509
  }
410
510
 
411
511
  CGFloat peekContentHeight = [_containerView peekContentHeight];
@@ -471,7 +571,7 @@ using namespace facebook::react;
471
571
  [_controller setupSheetSizing];
472
572
  [_controller setupSheetProps];
473
573
 
474
- // Pin before measuring so a first-time pin doesn't shift sizes mid-animation.
574
+ // Detect before measuring so a first-time detection doesn't shift sizes mid-animation.
475
575
  [self setupScrollable];
476
576
 
477
577
  // Measure synchronously so the presentation starts at the right height —
@@ -480,24 +580,41 @@ using namespace facebook::react;
480
580
  if (_containerView) {
481
581
  _controller.contentHeight = @([_containerView contentHeight]);
482
582
  _controller.headerHeight = @([_containerView headerHeight]);
483
- _controller.footerHeight = @([_containerView footerHeight]);
583
+ [self syncFooterMetrics];
484
584
  _controller.peekContentHeight = @([_containerView peekContentHeight]);
485
585
  }
486
586
  _pendingSizeChange = NO;
487
587
 
588
+ [self refreshFooterBottomInset];
488
589
  [_controller setupSheetDetents];
489
590
  [_controller setupActiveDetentWithIndex:index];
490
591
 
491
592
  [_screensEventObserver capturePresenterScreenFromView:self];
492
593
  [_screensEventObserver startObservingWithState:_state.get()->getData()];
493
594
 
595
+ UIWindow *window = presentingViewController.view.window;
494
596
  [presentingViewController presentViewController:_controller
495
597
  animated:animated
496
598
  completion:^{
599
+ [TrueSheetOverlayView bringOverlaysToFrontInWindow:window];
497
600
  if (completion) {
498
601
  completion(YES, nil);
499
602
  }
500
603
  }];
604
+
605
+ // Presenting adds the sheet's transition view on top of the window once the
606
+ // transition starts — re-front overlays alongside it so the first frame is
607
+ // already ordered (and again on completion in case the transition was queued)
608
+ id<UIViewControllerTransitionCoordinator> coordinator = _controller.transitionCoordinator;
609
+ if (coordinator) {
610
+ [coordinator
611
+ animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
612
+ [TrueSheetOverlayView bringOverlaysToFrontInWindow:window];
613
+ }
614
+ completion:nil];
615
+ } else {
616
+ [TrueSheetOverlayView bringOverlaysToFrontInWindow:window];
617
+ }
501
618
  }
502
619
 
503
620
  - (void)resizeToIndex:(NSInteger)index completion:(nullable TrueSheetCompletionBlock)completion {
@@ -583,6 +700,10 @@ using namespace facebook::react;
583
700
  * Debounced sheet update to handle rapid content/header size changes.
584
701
  */
585
702
  - (void)setupSheetDetentsForSizeChange {
703
+ // Keep the footer's absorbed inset in sync with the latest measured heights
704
+ // even before presenting, so the sheet presents at its final size.
705
+ [self refreshFooterBottomInset];
706
+
586
707
  // Retargeting the in-flight presentation makes UIKit snap the whole stack
587
708
  // (including the sheet behind) instead of animating. Defer to didPresent —
588
709
  // presentAtIndex measured synchronously, so this is usually a no-op.
@@ -610,6 +731,7 @@ using namespace facebook::react;
610
731
  // Refresh here (not just on peek size events) since the peek's offset
611
732
  // within the content can change without its own size changing.
612
733
  self->_controller.peekContentHeight = @([self->_containerView peekContentHeight]);
734
+ [self refreshFooterBottomInset];
613
735
  [self->_controller setupSheetDetentsForSizeChange];
614
736
  });
615
737
  }
@@ -625,7 +747,7 @@ using namespace facebook::react;
625
747
  }
626
748
 
627
749
  - (void)containerViewFooterDidChangeSize:(CGSize)newSize {
628
- _controller.footerHeight = @(newSize.height);
750
+ [self syncFooterMetrics];
629
751
  [self setupSheetDetentsForSizeChange];
630
752
  [_controller setupAccessibilityContainer];
631
753
  }
@@ -634,7 +756,7 @@ using namespace facebook::react;
634
756
  [self setupSheetDetentsForSizeChange];
635
757
  }
636
758
 
637
- // When the ScrollView changes (e.g. conditional remount), re-pin the new ScrollView.
759
+ // When the ScrollView changes (e.g. conditional remount), re-detect the new ScrollView.
638
760
  - (void)containerViewScrollViewDidChange {
639
761
  [self setupScrollable];
640
762
  }
@@ -687,6 +809,10 @@ using namespace facebook::react;
687
809
  }
688
810
  }
689
811
 
812
+ - (void)viewControllerDidAttemptDismiss {
813
+ [TrueSheetLifecycleEvents emitDismissAttempt:_eventEmitter];
814
+ }
815
+
690
816
  - (void)viewControllerDidDismiss {
691
817
  [_containerView cleanupKeyboardObserver];
692
818
  if (!_dismissedByNavigation) {
@@ -716,6 +842,11 @@ using namespace facebook::react;
716
842
  [self updateStateWithSize:size];
717
843
  }
718
844
 
845
+ - (void)viewControllerSafeAreaInsetsDidChange {
846
+ [self refreshFooterBottomInset];
847
+ [_containerView updateFooterKeyboardOffset];
848
+ }
849
+
719
850
  - (void)viewControllerWillFocus {
720
851
  [TrueSheetFocusEvents emitWillFocus:_eventEmitter];
721
852
  }
@@ -781,16 +912,41 @@ using namespace facebook::react;
781
912
  if (!_containerView)
782
913
  return;
783
914
 
784
- _containerView.insetAdjustment = _insetAdjustment;
785
915
  _containerView.scrollableOptions = _scrollableOptions;
786
- _containerView.hasAutoDetent = _hasAutoDetent;
787
- _containerView.footerBottomInset = _controller.footerBottomInset;
916
+ _containerView.scrollableHandle = _scrollableHandle;
917
+ _containerView.safeAreaInsetAdjustment = (_scrollableOptions ? _scrollableOptions.safeAreaInsetAdjustment : YES) &&
918
+ _insetAdjustment == TrueSheetViewInsetAdjustment::Automatic;
919
+ _containerView.footerInsetAdjustment = _controller.footerInsetAdjustment;
920
+ [self refreshFooterBottomInset];
788
921
  [_containerView setupScrollable];
922
+ }
923
+
924
+ // footerHeight and appliedFooterBottomInset must stay paired — detent math
925
+ // subtracts exactly the inset baked into the measured footer height (see
926
+ // autoDetentBottomInset/peekDetentBottomInset).
927
+ - (void)syncFooterMetrics {
928
+ _controller.footerHeight = @([_containerView footerHeight]);
929
+ _controller.appliedFooterBottomInset = [_containerView footerAppliedBottomInset];
930
+ }
931
+
932
+ // Recomputes the inset the footer absorbs and pushes it down. The inset
933
+ // depends on measured heights and detents (see maxNaturalDetentHeight), so
934
+ // this runs on size and detent changes — not just prop updates.
935
+ - (void)refreshFooterBottomInset {
936
+ if (!_containerView)
937
+ return;
938
+
939
+ // appliedFooterBottomInset is NOT set here — it tracks what's actually
940
+ // baked into the footer's measured height (reported with its layout), not
941
+ // what was just pushed. Setting it at push time desyncs the pair and lets
942
+ // a seeded inset read as natural content height (see maxNaturalDetentHeight).
943
+ CGFloat inset = _controller.footerBottomInset;
944
+ _containerView.footerBottomInset = inset;
789
945
 
790
946
  // Publish the inset so a footer set later (e.g. navigation setOptions) is
791
947
  // padded on its very first layout instead of resizing the sheet twice.
792
948
  if (_insetAdjustment == TrueSheetViewInsetAdjustment::Automatic) {
793
- TrueSheetInsets::setBottomSafeArea(_controller.footerBottomInset);
949
+ TrueSheetInsets::setBottomSafeArea(inset);
794
950
  }
795
951
  }
796
952
 
@@ -801,6 +957,7 @@ using namespace facebook::react;
801
957
  if (_pendingDetents) {
802
958
  _controller.detents = _pendingDetents;
803
959
  _pendingDetents = nil;
960
+ [self refreshFooterBottomInset];
804
961
  }
805
962
 
806
963
  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;
@@ -53,6 +55,7 @@ NS_ASSUME_NONNULL_BEGIN
53
55
 
54
56
  @property (nonatomic, weak, nullable) id<TrueSheetViewControllerDelegate> delegate;
55
57
  @property (nonatomic, strong) NSArray<NSNumber *> *detents;
58
+ @property (nonatomic, copy) NSArray<NSDictionary *> *detentBackgrounds;
56
59
  @property (nonatomic, strong, nullable) NSNumber *maxContentHeight;
57
60
  @property (nonatomic, strong, nullable) NSNumber *maxContentWidth;
58
61
  @property (nonatomic, strong, nullable) NSNumber *contentHeight;
@@ -60,6 +63,13 @@ NS_ASSUME_NONNULL_BEGIN
60
63
  @property (nonatomic, assign) BOOL absoluteHeader;
61
64
  @property (nonatomic, strong, nullable) NSNumber *footerHeight;
62
65
  @property (nonatomic, assign) BOOL absoluteFooter;
66
+
67
+ /**
68
+ * YES while the plugged scrollable is padded by the absolute footer's height
69
+ * (see ScrollableOptions.contentInsetAdjustment) — the footer then counts
70
+ * toward the auto detent like a relative one, so the content ends above it.
71
+ */
72
+ @property (nonatomic, assign) BOOL footerInsetAdjustment;
63
73
  @property (nonatomic, strong, nullable) NSNumber *peekContentHeight;
64
74
  @property (nonatomic, strong, nullable) UIColor *backgroundColor;
65
75
  @property (nonatomic, strong, nullable) NSNumber *cornerRadius;
@@ -70,13 +80,17 @@ NS_ASSUME_NONNULL_BEGIN
70
80
  @property (nonatomic, assign) BOOL dimmed;
71
81
  @property (nonatomic, strong, nullable) NSNumber *dimmedDetentIndex;
72
82
  @property (nonatomic, assign) facebook::react::TrueSheetViewBackgroundBlur backgroundBlur;
73
- @property (nonatomic, strong, nullable) NSNumber *blurIntensity;
74
- @property (nonatomic, assign) BOOL blurInteraction;
75
83
  @property (nonatomic, assign) facebook::react::TrueSheetViewPresentation presentation;
76
- @property (nonatomic, assign) facebook::react::TrueSheetViewAnchor anchor;
84
+ @property (nonatomic, assign) facebook::react::TrueSheetViewPlacement placement;
77
85
  @property (nonatomic, assign) facebook::react::TrueSheetViewInsetAdjustment insetAdjustment;
78
86
  @property (nonatomic, assign) BOOL scrollingExpandsSheet;
79
87
  @property (nonatomic, assign) CGFloat footerKeyboardOffset;
88
+
89
+ /**
90
+ * NO keeps an absolute footer pinned to the sheet's bottom edge behind the
91
+ * keyboard instead of rising above it.
92
+ */
93
+ @property (nonatomic, assign) BOOL footerAvoidsKeyboard;
80
94
  @property (nonatomic, assign) BOOL dismissible;
81
95
  @property (nonatomic, assign) BOOL isPresented;
82
96
  @property (nonatomic, assign) NSInteger activeDetentIndex;
@@ -87,6 +101,12 @@ NS_ASSUME_NONNULL_BEGIN
87
101
  * ~keyboardHeight into the learned offset.
88
102
  */
89
103
  @property (nonatomic, assign) BOOL keyboardSheetGrown;
104
+
105
+ /**
106
+ * Target keyboard height, set as soon as the keyboard frame change is
107
+ * announced — ahead of the grow/shrink animation, unlike keyboardSheetGrown.
108
+ */
109
+ @property (nonatomic, assign) CGFloat keyboardHeight;
90
110
  @property (nonatomic, readonly) BOOL isTopmostPresentedController;
91
111
  @property (nonatomic, readonly) BOOL isStackedBehindChild;
92
112
  @property (nonatomic, weak, nullable) UIView *accessibilityContentView;
@@ -99,6 +119,14 @@ NS_ASSUME_NONNULL_BEGIN
99
119
  */
100
120
  @property (nonatomic, readonly) CGFloat footerBottomInset;
101
121
 
122
+ /**
123
+ * The inset currently baked into the footer's measured height, reported
124
+ * alongside its layout (see TrueSheetFooterView.appliedBottomInset). Detent
125
+ * resolution subtracts this exact value so the baked inset is never
126
+ * double-counted.
127
+ */
128
+ @property (nonatomic, assign) CGFloat appliedFooterBottomInset;
129
+
102
130
  - (void)setupAccessibilityContainer;
103
131
  - (void)applyActiveDetent;
104
132
  - (void)setupActiveDetentWithIndex:(NSInteger)index;