@lodev09/react-native-true-sheet 4.0.0-beta.3 → 4.0.0-beta.30

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 (244) hide show
  1. package/README.md +28 -5
  2. package/android/build.gradle +23 -1
  3. package/android/proguard-rules.pro +3 -0
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +50 -16
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +303 -184
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +5 -1
  7. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +38 -109
  8. package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +13 -0
  9. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +127 -0
  10. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayViewManager.kt +45 -0
  11. package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +2 -1
  12. package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +29 -3
  13. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +234 -66
  14. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +491 -186
  15. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +64 -10
  16. package/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +31 -14
  17. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetBehavior.kt +135 -0
  18. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +23 -23
  19. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +130 -6
  20. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +104 -39
  21. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +14 -17
  22. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +2 -4
  23. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetOverlayRootView.kt +148 -0
  24. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +33 -40
  25. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +15 -0
  26. package/android/src/main/java/com/lodev09/truesheet/utils/ColorUtils.kt +19 -0
  27. package/android/src/main/java/com/lodev09/truesheet/utils/KeyboardUtils.kt +15 -2
  28. package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +0 -12
  29. package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +19 -7
  30. package/android/src/main/jni/TrueSheetSpec.h +1 -0
  31. package/android/src/main/jni/TrueSheetStateUpdater.cpp +37 -6
  32. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +1 -9
  33. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +114 -30
  34. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +30 -7
  35. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +2 -2
  36. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +9 -8
  37. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h +3 -5
  38. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +14 -3
  39. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +5 -6
  40. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +1 -1
  41. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +10 -6
  42. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.cpp +1 -1
  43. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.h +1 -1
  44. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetLayoutUtils.h +43 -0
  45. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewComponentDescriptor.h +21 -0
  46. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.cpp +17 -0
  47. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.h +33 -0
  48. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h +3 -5
  49. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +7 -40
  50. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.h +3 -7
  51. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.cpp +3 -5
  52. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.h +4 -5
  53. package/ios/TrueSheetContainerView.h +23 -7
  54. package/ios/TrueSheetContainerView.mm +45 -25
  55. package/ios/TrueSheetContentView.h +44 -15
  56. package/ios/TrueSheetContentView.mm +221 -190
  57. package/ios/TrueSheetFooterView.mm +133 -28
  58. package/ios/TrueSheetModule.h +2 -7
  59. package/ios/TrueSheetModule.mm +18 -18
  60. package/ios/TrueSheetOverlayView.h +42 -0
  61. package/ios/TrueSheetOverlayView.mm +175 -0
  62. package/ios/TrueSheetView.h +2 -0
  63. package/ios/TrueSheetView.mm +268 -52
  64. package/ios/TrueSheetViewController.h +36 -3
  65. package/ios/TrueSheetViewController.mm +797 -102
  66. package/ios/core/RNScreensEventObserver.mm +31 -28
  67. package/ios/core/TrueSheetBlurView.h +0 -2
  68. package/ios/core/TrueSheetBlurView.mm +3 -36
  69. package/ios/core/TrueSheetDetentCalculator.h +18 -1
  70. package/ios/core/TrueSheetDetentCalculator.mm +44 -9
  71. package/ios/core/TrueSheetDimView.h +27 -0
  72. package/ios/core/TrueSheetDimView.mm +41 -0
  73. package/ios/core/TrueSheetGrabberView.mm +13 -3
  74. package/ios/core/TrueSheetKeyboardObserver.h +7 -0
  75. package/ios/core/TrueSheetKeyboardObserver.mm +107 -15
  76. package/ios/core/TrueSheetOverlayContainerView.h +31 -0
  77. package/ios/core/TrueSheetOverlayContainerView.mm +35 -0
  78. package/ios/events/TrueSheetLifecycleEvents.h +2 -0
  79. package/ios/events/TrueSheetLifecycleEvents.mm +8 -0
  80. package/ios/tvos/TrueSheetStubs.mm +12 -0
  81. package/ios/utils/UIView+ScrollEdgeInteraction.h +1 -0
  82. package/ios/utils/UIView+ScrollEdgeInteraction.mm +24 -2
  83. package/lib/module/TrueSheet.js +222 -54
  84. package/lib/module/TrueSheet.js.map +1 -1
  85. package/lib/module/TrueSheet.web.js +364 -275
  86. package/lib/module/TrueSheet.web.js.map +1 -1
  87. package/lib/module/TrueSheetBackground.js +15 -0
  88. package/lib/module/TrueSheetBackground.js.map +1 -0
  89. package/lib/module/TrueSheetOverlay.js +30 -0
  90. package/lib/module/TrueSheetOverlay.js.map +1 -0
  91. package/lib/module/TrueSheetOverlay.web.js +35 -0
  92. package/lib/module/TrueSheetOverlay.web.js.map +1 -0
  93. package/lib/module/TrueSheetPeek.web.js +20 -15
  94. package/lib/module/TrueSheetPeek.web.js.map +1 -1
  95. package/lib/module/TrueSheetProvider.web.js +13 -9
  96. package/lib/module/TrueSheetProvider.web.js.map +1 -1
  97. package/lib/module/detentBackgrounds.js +21 -0
  98. package/lib/module/detentBackgrounds.js.map +1 -0
  99. package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  100. package/lib/module/fabric/TrueSheetFooterViewNativeComponent.ts +2 -3
  101. package/lib/module/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  102. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +89 -73
  103. package/lib/module/index.js +1 -0
  104. package/lib/module/index.js.map +1 -1
  105. package/lib/module/mocks/index.js +18 -2
  106. package/lib/module/mocks/index.js.map +1 -1
  107. package/lib/module/mocks/navigationExpoRouter.js.map +1 -1
  108. package/lib/module/navigation/base-types.js +2 -0
  109. package/lib/module/navigation/base-types.js.map +1 -0
  110. package/lib/module/navigation/createTrueSheetRouter.js +158 -100
  111. package/lib/module/navigation/createTrueSheetRouter.js.map +1 -1
  112. package/lib/module/navigation/expo-router/base-types.js +2 -0
  113. package/lib/module/navigation/expo-router/base-types.js.map +1 -0
  114. package/lib/module/navigation/expo-router/extendTrueSheetRouter.js +71 -0
  115. package/lib/module/navigation/expo-router/extendTrueSheetRouter.js.map +1 -0
  116. package/lib/module/navigation/expo-router/index.js +18 -7
  117. package/lib/module/navigation/expo-router/index.js.map +1 -1
  118. package/lib/module/navigation/expo-router/types.js +4 -0
  119. package/lib/module/navigation/expo-router/types.js.map +1 -0
  120. package/lib/module/navigation/index.js.map +1 -1
  121. package/lib/module/navigation/screen/useSheetScreenState.js +1 -0
  122. package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -1
  123. package/lib/module/scrollable.js +28 -0
  124. package/lib/module/scrollable.js.map +1 -0
  125. package/lib/module/web/constants.js +1 -1
  126. package/lib/module/web/constants.js.map +1 -1
  127. package/lib/module/web/keyboard.js +69 -0
  128. package/lib/module/web/keyboard.js.map +1 -0
  129. package/lib/module/web/layout.js +168 -0
  130. package/lib/module/web/layout.js.map +1 -0
  131. package/lib/module/web/vaul/constants.js +9 -0
  132. package/lib/module/web/vaul/constants.js.map +1 -1
  133. package/lib/module/web/vaul/context.js +4 -0
  134. package/lib/module/web/vaul/context.js.map +1 -1
  135. package/lib/module/web/vaul/helpers.js +1 -0
  136. package/lib/module/web/vaul/helpers.js.map +1 -1
  137. package/lib/module/web/vaul/index.js +83 -21
  138. package/lib/module/web/vaul/index.js.map +1 -1
  139. package/lib/module/web/vaul/use-snap-points.js +57 -28
  140. package/lib/module/web/vaul/use-snap-points.js.map +1 -1
  141. package/lib/typescript/src/TrueSheet.d.ts +18 -1
  142. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  143. package/lib/typescript/src/TrueSheet.types.d.ts +209 -35
  144. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  145. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  146. package/lib/typescript/src/TrueSheetBackground.d.ts +3 -0
  147. package/lib/typescript/src/TrueSheetBackground.d.ts.map +1 -0
  148. package/lib/typescript/src/TrueSheetOverlay.d.ts +8 -0
  149. package/lib/typescript/src/TrueSheetOverlay.d.ts.map +1 -0
  150. package/lib/typescript/src/TrueSheetOverlay.web.d.ts +8 -0
  151. package/lib/typescript/src/TrueSheetOverlay.web.d.ts.map +1 -0
  152. package/lib/typescript/src/TrueSheetPeek.web.d.ts +8 -4
  153. package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
  154. package/lib/typescript/src/TrueSheetProvider.web.d.ts +5 -1
  155. package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -1
  156. package/lib/typescript/src/detentBackgrounds.d.ts +10 -0
  157. package/lib/typescript/src/detentBackgrounds.d.ts.map +1 -0
  158. package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts +2 -3
  159. package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts.map +1 -1
  160. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts +6 -0
  161. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts.map +1 -0
  162. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +76 -65
  163. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  164. package/lib/typescript/src/index.d.ts +1 -0
  165. package/lib/typescript/src/index.d.ts.map +1 -1
  166. package/lib/typescript/src/mocks/index.d.ts +4 -0
  167. package/lib/typescript/src/mocks/index.d.ts.map +1 -1
  168. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +3 -3
  169. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -1
  170. package/lib/typescript/src/navigation/base-types.d.ts +9 -0
  171. package/lib/typescript/src/navigation/base-types.d.ts.map +1 -0
  172. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +36 -10
  173. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -1
  174. package/lib/typescript/src/navigation/expo-router/base-types.d.ts +8 -0
  175. package/lib/typescript/src/navigation/expo-router/base-types.d.ts.map +1 -0
  176. package/lib/typescript/src/navigation/expo-router/extendTrueSheetRouter.d.ts +33 -0
  177. package/lib/typescript/src/navigation/expo-router/extendTrueSheetRouter.d.ts.map +1 -0
  178. package/lib/typescript/src/navigation/expo-router/index.d.ts +9 -6
  179. package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -1
  180. package/lib/typescript/src/navigation/expo-router/types.d.ts +44 -0
  181. package/lib/typescript/src/navigation/expo-router/types.d.ts.map +1 -0
  182. package/lib/typescript/src/navigation/index.d.ts +3 -4
  183. package/lib/typescript/src/navigation/index.d.ts.map +1 -1
  184. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +2 -1
  185. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -1
  186. package/lib/typescript/src/navigation/types.d.ts +43 -6
  187. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  188. package/lib/typescript/src/scrollable.d.ts +2 -0
  189. package/lib/typescript/src/scrollable.d.ts.map +1 -0
  190. package/lib/typescript/src/web/constants.d.ts +1 -1
  191. package/lib/typescript/src/web/constants.d.ts.map +1 -1
  192. package/lib/typescript/src/web/keyboard.d.ts +15 -0
  193. package/lib/typescript/src/web/keyboard.d.ts.map +1 -0
  194. package/lib/typescript/src/web/layout.d.ts +20 -0
  195. package/lib/typescript/src/web/layout.d.ts.map +1 -0
  196. package/lib/typescript/src/web/vaul/constants.d.ts +3 -0
  197. package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -1
  198. package/lib/typescript/src/web/vaul/context.d.ts +5 -1
  199. package/lib/typescript/src/web/vaul/context.d.ts.map +1 -1
  200. package/lib/typescript/src/web/vaul/helpers.d.ts +1 -0
  201. package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
  202. package/lib/typescript/src/web/vaul/index.d.ts +20 -1
  203. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
  204. package/lib/typescript/src/web/vaul/types.d.ts +9 -0
  205. package/lib/typescript/src/web/vaul/types.d.ts.map +1 -1
  206. package/lib/typescript/src/web/vaul/use-snap-points.d.ts +6 -4
  207. package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
  208. package/package.json +18 -8
  209. package/react-native.config.js +1 -0
  210. package/src/TrueSheet.tsx +257 -62
  211. package/src/TrueSheet.types.ts +225 -35
  212. package/src/TrueSheet.web.tsx +445 -333
  213. package/src/TrueSheetBackground.tsx +17 -0
  214. package/src/TrueSheetOverlay.tsx +24 -0
  215. package/src/TrueSheetOverlay.web.tsx +33 -0
  216. package/src/TrueSheetPeek.web.tsx +25 -23
  217. package/src/TrueSheetProvider.web.tsx +22 -13
  218. package/src/detentBackgrounds.ts +24 -0
  219. package/src/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  220. package/src/fabric/TrueSheetFooterViewNativeComponent.ts +2 -3
  221. package/src/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  222. package/src/fabric/TrueSheetViewNativeComponent.ts +89 -73
  223. package/src/index.ts +1 -0
  224. package/src/mocks/index.ts +17 -2
  225. package/src/mocks/navigationExpoRouter.ts +6 -5
  226. package/src/navigation/base-types.ts +20 -0
  227. package/src/navigation/createTrueSheetRouter.ts +220 -144
  228. package/src/navigation/expo-router/base-types.ts +18 -0
  229. package/src/navigation/expo-router/extendTrueSheetRouter.ts +99 -0
  230. package/src/navigation/expo-router/index.ts +41 -30
  231. package/src/navigation/expo-router/types.ts +116 -0
  232. package/src/navigation/index.ts +6 -7
  233. package/src/navigation/screen/useSheetScreenState.ts +2 -0
  234. package/src/navigation/types.ts +81 -23
  235. package/src/scrollable.ts +25 -0
  236. package/src/web/constants.ts +1 -1
  237. package/src/web/keyboard.ts +79 -0
  238. package/src/web/layout.ts +196 -0
  239. package/src/web/vaul/constants.ts +9 -0
  240. package/src/web/vaul/context.ts +7 -1
  241. package/src/web/vaul/helpers.ts +5 -0
  242. package/src/web/vaul/index.tsx +113 -16
  243. package/src/web/vaul/types.ts +10 -0
  244. package/src/web/vaul/use-snap-points.ts +86 -33
@@ -67,7 +67,7 @@ using namespace facebook::react;
67
67
  return;
68
68
  }
69
69
  _bottomInset = bottomInset;
70
- [self pushBottomInsetState];
70
+ [self pushStateWithMode:EventQueue::UpdateMode::unstable_Immediate];
71
71
  }
72
72
 
73
73
  // Skipped while the keyboard is open — an absolute footer rises above the
@@ -77,22 +77,36 @@ using namespace facebook::react;
77
77
  return;
78
78
  }
79
79
  _keyboardVisible = keyboardVisible;
80
- [self pushBottomInsetState];
80
+ [self pushStateWithMode:EventQueue::UpdateMode::unstable_Immediate];
81
81
  }
82
82
 
83
- - (void)pushBottomInsetState {
83
+ // The keyboard slide is a transform the shadow tree can't see — mirror it so
84
+ // measure() (and Pressability's press region) match where the footer is drawn.
85
+ // Async: no layout reads it.
86
+ - (void)setKeyboardOffset:(CGFloat)keyboardOffset {
87
+ if (_currentKeyboardOffset == keyboardOffset) {
88
+ return;
89
+ }
90
+ _currentKeyboardOffset = keyboardOffset;
91
+ [self pushStateWithMode:EventQueue::UpdateMode::Asynchronous];
92
+ }
93
+
94
+ // Every push carries all fields — RN's EventQueue drops a still-queued update
95
+ // for the same view when the next one is enqueued.
96
+ - (void)pushStateWithMode:(EventQueue::UpdateMode)mode {
84
97
  if (!_state) {
85
98
  return;
86
99
  }
87
100
 
88
101
  TrueSheetFooterViewState newState;
89
102
  newState.bottomInset = _keyboardVisible ? 0 : _bottomInset;
103
+ newState.translateY = -_currentKeyboardOffset;
90
104
  newState.initialized = true;
91
- // Immediate so the inset lands before anything reads the footer's height —
92
- // the async path races the keyboard caret scroll and detent setup, leaving
93
- // them an inset stale (same reason Android bridges through
94
- // TrueSheetStateUpdater).
95
- _state->updateState(std::move(newState), EventQueue::UpdateMode::unstable_Immediate);
105
+ // Inset changes go Immediate so they land before anything reads the
106
+ // footer's height — the async path races the keyboard caret scroll and
107
+ // detent setup, leaving them an inset stale (same reason Android bridges
108
+ // through TrueSheetStateUpdater).
109
+ _state->updateState(std::move(newState), mode);
96
110
  _didPushBottomInset = YES;
97
111
  }
98
112
 
@@ -123,6 +137,11 @@ using namespace facebook::react;
123
137
 
124
138
  - (void)collectAccessibilityElementsFromView:(UIView *)view into:(NSMutableArray *)elements {
125
139
  for (UIView *subview in view.subviews) {
140
+ // Fabric hides `display: none` views instead of unmounting them
141
+ if (subview.hidden) {
142
+ continue;
143
+ }
144
+
126
145
  if (subview.isAccessibilityElement || subview.accessibilityLabel || subview.accessibilityIdentifier) {
127
146
  [elements addObject:subview];
128
147
  } else if (subview.accessibilityElements.count > 0) {
@@ -137,7 +156,19 @@ using namespace facebook::react;
137
156
 
138
157
  - (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics
139
158
  oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics {
140
- [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
159
+ // UIKit resizes the sheet for the keyboard inside its animation, and the new
160
+ // size usually relays out the footer in the same pass. When the keyboard
161
+ // changes during a React commit (e.g. its input unmounts), that layout can
162
+ // only mount after the commit, outside the animation — the footer, pinned to
163
+ // the container's bottom edge, would snap by the keyboard's height while the
164
+ // sheet is still animating. Carry it on the rest of the keyboard's curve.
165
+ if (layoutMetrics.frame.origin.y != oldLayoutMetrics.frame.origin.y) {
166
+ [self animateAlongKeyboard:^{
167
+ [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
168
+ }];
169
+ } else {
170
+ [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
171
+ }
141
172
 
142
173
  // State lands before layout metrics within the same mount transaction, so
143
174
  // this reflects the inset the shadow node baked into this frame — seeded
@@ -155,6 +186,8 @@ using namespace facebook::react;
155
186
  _lastHeight = height;
156
187
  [self.delegate footerViewDidChangeSize:CGSizeMake(layoutMetrics.frame.size.width, height)];
157
188
  }
189
+
190
+ [self applyKeyboardOffset];
158
191
  }
159
192
 
160
193
  - (void)prepareForRecycle {
@@ -176,55 +209,127 @@ using namespace facebook::react;
176
209
 
177
210
  #pragma mark - TrueSheetKeyboardObserverDelegate
178
211
 
212
+ - (void)setKeyboardObserver:(TrueSheetKeyboardObserver *)keyboardObserver {
213
+ [_keyboardObserver removeDelegate:self];
214
+ _keyboardObserver = keyboardObserver;
215
+ [keyboardObserver addDelegate:self];
216
+ }
217
+
218
+ - (void)animateAlongKeyboard:(void (^)(void))changes {
219
+ NSTimeInterval remaining = self.keyboardObserver.remainingAnimationDuration;
220
+ if (remaining > 0 && [UIView inheritedAnimationDuration] == 0) {
221
+ [UIView animateWithDuration:remaining
222
+ delay:0
223
+ options:self.keyboardObserver.animationCurve | UIViewAnimationOptionBeginFromCurrentState
224
+ animations:^{
225
+ changes();
226
+ [self syncEdgeEffect];
227
+ }
228
+ completion:nil];
229
+ } else {
230
+ changes();
231
+ }
232
+ }
233
+
234
+ // Only an absolute footer floats above the keyboard — a relative footer stays
235
+ // in the layout flow behind it, and an absolute one opts out via
236
+ // footerOptions.avoidKeyboard.
237
+ - (BOOL)floatsAboveKeyboard {
238
+ TrueSheetViewController *controller = self.keyboardObserver.viewController;
239
+ return controller.absoluteFooter && controller.footerAvoidsKeyboard;
240
+ }
241
+
242
+ - (CGFloat)keyboardSlideForHeight:(CGFloat)height {
243
+ UIWindow *window = self.window;
244
+ if (!window) {
245
+ return 0;
246
+ }
247
+
248
+ // Measure in the transform's coordinate space, excluding the footer's current slide.
249
+ CGFloat footerBottom = self.center.y + self.bounds.size.height / 2;
250
+ CGPoint keyboardTopInWindow = CGPointMake(CGRectGetMidX(window.bounds), CGRectGetMaxY(window.bounds) - height);
251
+ CGFloat keyboardTop = [self.superview convertPoint:keyboardTopInWindow fromView:window].y;
252
+ CGFloat keyboardOffset = self.keyboardObserver.viewController.footerKeyboardOffset;
253
+ return MAX(0, footerBottom - keyboardTop + keyboardOffset);
254
+ }
255
+
179
256
  // Yoga owns the footer's frame (pinned to the container's bottom edge), so the
180
- // keyboard slide is carried by a transform instead of layout. Only an absolute
181
- // footer floats above the keyboard — a relative footer stays in the layout
182
- // flow behind it.
257
+ // keyboard slide is carried by a transform instead of layout.
183
258
  - (void)keyboardWillShow:(CGFloat)height duration:(NSTimeInterval)duration curve:(UIViewAnimationOptions)curve {
184
- if (!self.keyboardObserver.viewController.absoluteFooter) {
259
+ if (![self floatsAboveKeyboard]) {
185
260
  return;
186
261
  }
187
262
 
188
263
  [self setKeyboardVisible:YES];
189
264
 
190
- CGFloat keyboardOffset = self.keyboardObserver.viewController.footerKeyboardOffset;
191
- CGFloat slide = MAX(0, height + keyboardOffset);
192
- _currentKeyboardOffset = slide;
265
+ CGFloat slide = [self keyboardSlideForHeight:height];
266
+ [self setKeyboardOffset:slide];
193
267
 
194
268
  [UIView animateWithDuration:duration
195
- delay:0
196
- options:curve | UIViewAnimationOptionBeginFromCurrentState
197
- animations:^{
198
- self.transform = CGAffineTransformMakeTranslation(0, -slide);
199
- }
200
- completion:nil];
269
+ delay:0
270
+ options:curve | UIViewAnimationOptionBeginFromCurrentState
271
+ animations:^{
272
+ self.transform = CGAffineTransformMakeTranslation(0, -slide);
273
+ [self syncEdgeEffect];
274
+ }
275
+ completion:^(BOOL finished) {
276
+ [self applyKeyboardOffset];
277
+ }];
201
278
  }
202
279
 
203
280
  - (void)keyboardWillHide:(NSTimeInterval)duration curve:(UIViewAnimationOptions)curve {
281
+ // Offset first — the visible push replaces it in the queue, one commit
282
+ [self setKeyboardOffset:0];
204
283
  [self setKeyboardVisible:NO];
205
- _currentKeyboardOffset = 0;
206
284
 
207
285
  [UIView animateWithDuration:duration
208
286
  delay:0
209
287
  options:curve | UIViewAnimationOptionBeginFromCurrentState
210
288
  animations:^{
211
289
  self.transform = CGAffineTransformIdentity;
290
+ [self syncEdgeEffect];
212
291
  }
213
292
  completion:nil];
214
293
  }
215
294
 
295
+ // UIKit derives the edge effect region from model geometry at commit time,
296
+ // outside our animation context — the blur snaps to the footer's final
297
+ // position while the footer is still animating. Re-evaluating the scroll
298
+ // view's layout inside the animation block makes the region change inherit
299
+ // the keyboard curve.
300
+ - (void)syncEdgeEffect {
301
+ if (@available(iOS 26.0, *)) {
302
+ UIScrollView *scrollView = [self edgeInteractionScrollView];
303
+ if (scrollView) {
304
+ [scrollView setNeedsLayout];
305
+ [scrollView layoutIfNeeded];
306
+ }
307
+ }
308
+ }
309
+
216
310
  - (void)applyKeyboardOffset {
217
311
  CGFloat height = self.keyboardObserver.currentHeight;
218
- if (height <= 0 || !self.keyboardObserver.viewController.absoluteFooter) {
312
+ if (height <= 0) {
313
+ return;
314
+ }
315
+
316
+ // avoidKeyboard flipped off mid-keyboard — drop the slide already applied
317
+ if (![self floatsAboveKeyboard]) {
318
+ [self setKeyboardOffset:0];
319
+ [self setKeyboardVisible:NO];
320
+ self.transform = CGAffineTransformIdentity;
219
321
  return;
220
322
  }
221
323
 
222
324
  [self setKeyboardVisible:YES];
223
325
 
224
- CGFloat keyboardOffset = self.keyboardObserver.viewController.footerKeyboardOffset;
225
- CGFloat slide = MAX(0, height + keyboardOffset);
226
- _currentKeyboardOffset = slide;
227
- self.transform = CGAffineTransformMakeTranslation(0, -slide);
326
+ CGFloat slide = [self keyboardSlideForHeight:height];
327
+ [self setKeyboardOffset:slide];
328
+ // A footer mounted or relaid out mid-keyboard animation follows the keyboard
329
+ // instead of snapping ahead of it
330
+ [self animateAlongKeyboard:^{
331
+ self.transform = CGAffineTransformMakeTranslation(0, -slide);
332
+ }];
228
333
  }
229
334
 
230
335
  @end
@@ -27,16 +27,11 @@ NS_ASSUME_NONNULL_BEGIN
27
27
 
28
28
  /**
29
29
  * Register a sheet component view with its React tag
30
- * Called automatically by TrueSheetView during initialization
30
+ * Called automatically by TrueSheetView when it attaches to a window.
31
+ * The registry holds views weakly, so no unregistration is needed.
31
32
  */
32
33
  + (void)registerView:(TrueSheetView *)view withTag:(NSNumber *)tag;
33
34
 
34
- /**
35
- * Unregister a sheet component view
36
- * Called automatically by TrueSheetView during dealloc
37
- */
38
- + (void)unregisterViewWithTag:(NSNumber *)tag;
39
-
40
35
  @end
41
36
 
42
37
  NS_ASSUME_NONNULL_END
@@ -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
 
@@ -148,11 +149,17 @@ RCT_EXPORT_MODULE(TrueSheetModule)
148
149
  - (void)dismissAll:(BOOL)animated resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
149
150
  RCTExecuteOnMainQueue(^{
150
151
  @synchronized(viewRegistry) {
152
+ // Drop presents still waiting on another sheet's dismissal
153
+ for (TrueSheetView *view in viewRegistry.objectEnumerator) {
154
+ [view cancelPendingPresent];
155
+ }
156
+
151
157
  // Find the root presented sheet (one without a parent TrueSheet)
152
158
  TrueSheetView *rootSheet = nil;
153
159
 
154
- for (TrueSheetView *view in viewRegistry.allValues) {
155
- if (!view.viewController.isPresented) {
160
+ for (TrueSheetView *view in viewRegistry.objectEnumerator) {
161
+ // A sheet still animating in counts too — dismissAnimated waits for it to land
162
+ if (!view.viewController.isPresented && !view.viewController.isBeingPresented) {
156
163
  continue;
157
164
  }
158
165
 
@@ -170,7 +177,10 @@ RCT_EXPORT_MODULE(TrueSheetModule)
170
177
  return;
171
178
  }
172
179
 
173
- [rootSheet emitDismissedPosition];
180
+ // Mid-present, the deferred dismiss tracks its own position like a regular dismiss
181
+ if (rootSheet.viewController.isPresented) {
182
+ [rootSheet emitDismissedPosition];
183
+ }
174
184
  [rootSheet dismissAnimated:animated
175
185
  completion:^(BOOL success, NSError *_Nullable error) {
176
186
  if (success) {
@@ -191,7 +201,7 @@ RCT_EXPORT_MODULE(TrueSheetModule)
191
201
  }
192
202
 
193
203
  @synchronized(viewRegistry) {
194
- return viewRegistry[reactTag];
204
+ return [viewRegistry objectForKey:reactTag];
195
205
  }
196
206
  }
197
207
 
@@ -201,17 +211,7 @@ RCT_EXPORT_MODULE(TrueSheetModule)
201
211
  }
202
212
 
203
213
  @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];
214
+ [viewRegistry setObject:view forKey:tag];
215
215
  }
216
216
  }
217
217
 
@@ -0,0 +1,42 @@
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
+ * Posted when an overlay container joins or leaves a window.
18
+ */
19
+ extern NSNotificationName const TrueSheetOverlayWindowDidChangeNotification;
20
+
21
+ /**
22
+ * Hidden host that renders its children in a container attached directly to
23
+ * the window, above every presented sheet.
24
+ */
25
+ @interface TrueSheetOverlayView : RCTViewComponentView
26
+
27
+ /**
28
+ * Re-stacks every overlay container above the window's other subviews.
29
+ * Called when a sheet presents, since presenting adds its transition view on top.
30
+ */
31
+ + (void)bringOverlaysToFrontInWindow:(nullable UIWindow *)window;
32
+
33
+ /**
34
+ * The overlay containers attached to the window, front to back.
35
+ */
36
+ + (NSArray<UIView *> *)overlayContainersInWindow:(nullable UIWindow *)window;
37
+
38
+ @end
39
+
40
+ NS_ASSUME_NONNULL_END
41
+
42
+ #endif
@@ -0,0 +1,175 @@
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
+ NSNotificationName const TrueSheetOverlayWindowDidChangeNotification = @"TrueSheetOverlayWindowDidChangeNotification";
25
+
26
+ static NSHashTable<TrueSheetOverlayView *> *gOverlayViews;
27
+
28
+ @interface TrueSheetOverlayView () <TrueSheetOverlayContainerViewDelegate>
29
+ @end
30
+
31
+ @implementation TrueSheetOverlayView {
32
+ TrueSheetOverlayContainerView *_containerView;
33
+ // Owns `_containerView` so it has a view controller in its responder chain. The
34
+ // container is a sibling of `rootViewController.view`, and a window's responder chain
35
+ // skips its root view controller — without an owner, `-[UIView reactViewController]`
36
+ // finds nothing and content that installs a view controller cannot attach.
37
+ UIViewController *_containerController;
38
+ RCTSurfaceTouchHandler *_touchHandler;
39
+ TrueSheetOverlayViewShadowNode::ConcreteState::Shared _state;
40
+ CGSize _lastStateSize;
41
+ }
42
+
43
+ + (ComponentDescriptorProvider)componentDescriptorProvider {
44
+ return concreteComponentDescriptorProvider<TrueSheetOverlayViewComponentDescriptor>();
45
+ }
46
+
47
+ + (void)bringOverlaysToFrontInWindow:(nullable UIWindow *)window {
48
+ if (!window)
49
+ return;
50
+
51
+ for (TrueSheetOverlayView *overlay in gOverlayViews) {
52
+ if (overlay->_containerView.superview == window) {
53
+ [window bringSubviewToFront:overlay->_containerView];
54
+ }
55
+ }
56
+ }
57
+
58
+ + (NSArray<UIView *> *)overlayContainersInWindow:(nullable UIWindow *)window {
59
+ NSMutableArray<UIView *> *containers = [NSMutableArray array];
60
+ for (UIView *subview in window.subviews.reverseObjectEnumerator) {
61
+ if ([subview isKindOfClass:[TrueSheetOverlayContainerView class]]) {
62
+ [containers addObject:subview];
63
+ }
64
+ }
65
+ return containers;
66
+ }
67
+
68
+ - (instancetype)initWithFrame:(CGRect)frame {
69
+ if (self = [super initWithFrame:frame]) {
70
+ static const auto defaultProps = std::make_shared<const TrueSheetOverlayViewProps>();
71
+ _props = defaultProps;
72
+
73
+ _containerView = [[TrueSheetOverlayContainerView alloc] initWithFrame:CGRectZero];
74
+ _containerView.delegate = self;
75
+
76
+ // Assigning the container as its view is what puts the controller in the responder
77
+ // chain; the containment below is for lifecycle only.
78
+ _containerController = [UIViewController new];
79
+ _containerController.view = _containerView;
80
+
81
+ _touchHandler = [[RCTSurfaceTouchHandler alloc] init];
82
+ [_touchHandler attachToView:_containerView];
83
+
84
+ _lastStateSize = CGSizeZero;
85
+
86
+ // The host stays in the React tree for layout only — children render in the container
87
+ self.hidden = YES;
88
+
89
+ if (!gOverlayViews) {
90
+ gOverlayViews = [NSHashTable weakObjectsHashTable];
91
+ }
92
+ [gOverlayViews addObject:self];
93
+ }
94
+ return self;
95
+ }
96
+
97
+ - (void)didMoveToWindow {
98
+ [super didMoveToWindow];
99
+
100
+ UIWindow *window = self.window;
101
+ if (window) {
102
+ _containerView.frame = window.bounds;
103
+ [window addSubview:_containerView];
104
+
105
+ UIViewController *root = window.rootViewController;
106
+ if (root != nil && _containerController.parentViewController != root) {
107
+ [root addChildViewController:_containerController];
108
+ [_containerController didMoveToParentViewController:root];
109
+ }
110
+ } else {
111
+ [_containerController willMoveToParentViewController:nil];
112
+ [_containerView removeFromSuperview];
113
+ [_containerController removeFromParentViewController];
114
+ }
115
+
116
+ [[NSNotificationCenter defaultCenter] postNotificationName:TrueSheetOverlayWindowDidChangeNotification object:nil];
117
+ }
118
+
119
+ #pragma mark - State
120
+
121
+ - (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState {
122
+ _state = std::static_pointer_cast<TrueSheetOverlayViewShadowNode::ConcreteState const>(state);
123
+
124
+ if (self.window) {
125
+ [self updateStateWithSize:self.window.bounds.size];
126
+ }
127
+ }
128
+
129
+ - (void)overlayContainerViewDidLayout:(CGSize)size {
130
+ [self updateStateWithSize:size];
131
+ }
132
+
133
+ - (void)updateStateWithSize:(CGSize)size {
134
+ if (!_state)
135
+ return;
136
+
137
+ if (fabs(size.width - _lastStateSize.width) < 0.5 && fabs(size.height - _lastStateSize.height) < 0.5)
138
+ return;
139
+
140
+ _lastStateSize = size;
141
+
142
+ auto stateData = _state->getData();
143
+ stateData.containerWidth = static_cast<float>(size.width);
144
+ stateData.containerHeight = static_cast<float>(size.height);
145
+ _state->updateState(std::move(stateData), EventQueue::UpdateMode::unstable_Immediate);
146
+ }
147
+
148
+ #pragma mark - Child Component Mounting
149
+
150
+ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
151
+ [_containerView insertSubview:childComponentView atIndex:index];
152
+ }
153
+
154
+ - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
155
+ [childComponentView removeFromSuperview];
156
+ }
157
+
158
+ - (void)prepareForRecycle {
159
+ [super prepareForRecycle];
160
+
161
+ [_containerController willMoveToParentViewController:nil];
162
+ [_containerView removeFromSuperview];
163
+ [_containerController removeFromParentViewController];
164
+ _lastStateSize = CGSizeZero;
165
+ _state.reset();
166
+ self.hidden = YES;
167
+ }
168
+
169
+ @end
170
+
171
+ Class<RCTComponentViewProtocol> TrueSheetOverlayViewCls(void) {
172
+ return TrueSheetOverlayView.class;
173
+ }
174
+
175
+ #endif
@@ -34,6 +34,8 @@ typedef void (^TrueSheetCompletionBlock)(BOOL success, NSError *_Nullable error)
34
34
 
35
35
  - (void)emitDismissedPosition;
36
36
 
37
+ - (BOOL)cancelPendingPresent;
38
+
37
39
  - (void)resizeToIndex:(NSInteger)index completion:(nullable TrueSheetCompletionBlock)completion;
38
40
 
39
41
  - (void)dismissStackAnimated:(BOOL)animated completion:(nullable TrueSheetCompletionBlock)completion;