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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/README.md +25 -4
  2. package/android/build.gradle +22 -1
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +24 -7
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +211 -182
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +5 -1
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +14 -19
  7. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +106 -0
  8. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayViewManager.kt +45 -0
  9. package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +2 -1
  10. package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +18 -0
  11. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +82 -39
  12. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +275 -88
  13. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +26 -1
  14. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +75 -3
  15. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +56 -34
  16. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetOverlayRootView.kt +137 -0
  17. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +15 -0
  18. package/android/src/main/java/com/lodev09/truesheet/utils/KeyboardUtils.kt +1 -1
  19. package/android/src/main/java/com/lodev09/truesheet/utils/TouchEventDeduper.kt +31 -0
  20. package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +19 -7
  21. package/android/src/main/jni/TrueSheetSpec.h +1 -0
  22. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +0 -8
  23. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +54 -29
  24. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +6 -1
  25. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +1 -1
  26. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +6 -4
  27. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +7 -0
  28. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetLayoutUtils.h +46 -0
  29. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewComponentDescriptor.h +21 -0
  30. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.cpp +17 -0
  31. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.h +36 -0
  32. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +4 -35
  33. package/ios/TrueSheetContainerView.h +14 -7
  34. package/ios/TrueSheetContainerView.mm +10 -7
  35. package/ios/TrueSheetContentView.h +31 -15
  36. package/ios/TrueSheetContentView.mm +133 -179
  37. package/ios/TrueSheetFooterView.h +6 -0
  38. package/ios/TrueSheetFooterView.mm +48 -2
  39. package/ios/TrueSheetModule.h +2 -7
  40. package/ios/TrueSheetModule.mm +7 -16
  41. package/ios/TrueSheetOverlayView.h +32 -0
  42. package/ios/TrueSheetOverlayView.mm +161 -0
  43. package/ios/TrueSheetView.mm +171 -29
  44. package/ios/TrueSheetViewController.h +10 -0
  45. package/ios/TrueSheetViewController.mm +256 -74
  46. package/ios/core/TrueSheetGrabberView.mm +13 -3
  47. package/ios/core/TrueSheetOverlayContainerView.h +31 -0
  48. package/ios/core/TrueSheetOverlayContainerView.mm +35 -0
  49. package/ios/events/TrueSheetLifecycleEvents.h +2 -0
  50. package/ios/events/TrueSheetLifecycleEvents.mm +8 -0
  51. package/ios/tvos/TrueSheetStubs.mm +12 -0
  52. package/ios/utils/UIView+ScrollEdgeInteraction.h +1 -0
  53. package/ios/utils/UIView+ScrollEdgeInteraction.mm +24 -2
  54. package/lib/module/TrueSheet.js +149 -34
  55. package/lib/module/TrueSheet.js.map +1 -1
  56. package/lib/module/TrueSheet.web.js +122 -53
  57. package/lib/module/TrueSheet.web.js.map +1 -1
  58. package/lib/module/TrueSheetOverlay.js +30 -0
  59. package/lib/module/TrueSheetOverlay.js.map +1 -0
  60. package/lib/module/TrueSheetOverlay.web.js +35 -0
  61. package/lib/module/TrueSheetOverlay.web.js.map +1 -0
  62. package/lib/module/TrueSheetPeek.web.js +3 -3
  63. package/lib/module/TrueSheetPeek.web.js.map +1 -1
  64. package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  65. package/lib/module/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  66. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +5 -0
  67. package/lib/module/index.js +1 -0
  68. package/lib/module/index.js.map +1 -1
  69. package/lib/module/mocks/index.js +10 -0
  70. package/lib/module/mocks/index.js.map +1 -1
  71. package/lib/module/mocks/navigation.js +31 -21
  72. package/lib/module/mocks/navigation.js.map +1 -1
  73. package/lib/module/mocks/navigationExpoRouter.js +21 -0
  74. package/lib/module/mocks/navigationExpoRouter.js.map +1 -0
  75. package/lib/module/mocks/reanimated.js +1 -1
  76. package/lib/module/mocks/reanimated.js.map +1 -1
  77. package/lib/module/navigation/{TrueSheetView.js → TrueSheetNavigatorContent.js} +14 -12
  78. package/lib/module/navigation/TrueSheetNavigatorContent.js.map +1 -0
  79. package/lib/module/navigation/actions.js +50 -0
  80. package/lib/module/navigation/actions.js.map +1 -0
  81. package/lib/module/navigation/base-types.js +2 -0
  82. package/lib/module/navigation/base-types.js.map +1 -0
  83. package/lib/module/navigation/{TrueSheetRouter.js → createTrueSheetRouter.js} +11 -24
  84. package/lib/module/navigation/createTrueSheetRouter.js.map +1 -0
  85. package/lib/module/navigation/expo-router/base-types.js +2 -0
  86. package/lib/module/navigation/expo-router/base-types.js.map +1 -0
  87. package/lib/module/navigation/expo-router/index.js +50 -0
  88. package/lib/module/navigation/expo-router/index.js.map +1 -0
  89. package/lib/module/navigation/expo-router/types.js +4 -0
  90. package/lib/module/navigation/expo-router/types.js.map +1 -0
  91. package/lib/module/navigation/index.js +43 -2
  92. package/lib/module/navigation/index.js.map +1 -1
  93. package/lib/module/navigation/navigator.js +10 -0
  94. package/lib/module/navigation/navigator.js.map +1 -0
  95. package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js +2 -2
  96. package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js.map +1 -1
  97. package/lib/module/navigation/screen/TrueSheetScreen.js +2 -2
  98. package/lib/module/navigation/screen/TrueSheetScreen.js.map +1 -1
  99. package/lib/module/navigation/screen/useSheetScreenState.js +5 -4
  100. package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -1
  101. package/lib/module/navigation/useTrueSheetNavigation.js +1 -1
  102. package/lib/module/navigation/useTrueSheetNavigation.js.map +1 -1
  103. package/lib/module/web/dom.js +10 -0
  104. package/lib/module/web/dom.js.map +1 -0
  105. package/lib/module/web/vaul/constants.js +4 -0
  106. package/lib/module/web/vaul/constants.js.map +1 -1
  107. package/lib/module/web/vaul/context.js.map +1 -1
  108. package/lib/module/web/vaul/helpers.js +10 -4
  109. package/lib/module/web/vaul/helpers.js.map +1 -1
  110. package/lib/module/web/vaul/index.js +35 -2
  111. package/lib/module/web/vaul/index.js.map +1 -1
  112. package/lib/typescript/src/TrueSheet.d.ts +6 -0
  113. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  114. package/lib/typescript/src/TrueSheet.types.d.ts +72 -1
  115. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  116. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  117. package/lib/typescript/src/TrueSheetOverlay.d.ts +8 -0
  118. package/lib/typescript/src/TrueSheetOverlay.d.ts.map +1 -0
  119. package/lib/typescript/src/TrueSheetOverlay.web.d.ts +8 -0
  120. package/lib/typescript/src/TrueSheetOverlay.web.d.ts.map +1 -0
  121. package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
  122. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts +6 -0
  123. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts.map +1 -0
  124. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +4 -0
  125. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  126. package/lib/typescript/src/index.d.ts +1 -0
  127. package/lib/typescript/src/index.d.ts.map +1 -1
  128. package/lib/typescript/src/mocks/index.d.ts +4 -0
  129. package/lib/typescript/src/mocks/index.d.ts.map +1 -1
  130. package/lib/typescript/src/mocks/navigation.d.ts +5 -1
  131. package/lib/typescript/src/mocks/navigation.d.ts.map +1 -1
  132. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +22 -0
  133. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -0
  134. package/lib/typescript/src/mocks/reanimated.d.ts.map +1 -1
  135. package/lib/typescript/src/navigation/TrueSheetNavigatorContent.d.ts +5 -0
  136. package/lib/typescript/src/navigation/TrueSheetNavigatorContent.d.ts.map +1 -0
  137. package/lib/typescript/src/navigation/actions.d.ts +71 -0
  138. package/lib/typescript/src/navigation/actions.d.ts.map +1 -0
  139. package/lib/typescript/src/navigation/base-types.d.ts +9 -0
  140. package/lib/typescript/src/navigation/base-types.d.ts.map +1 -0
  141. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +3 -0
  142. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -0
  143. package/lib/typescript/src/navigation/expo-router/base-types.d.ts +8 -0
  144. package/lib/typescript/src/navigation/expo-router/base-types.d.ts.map +1 -0
  145. package/lib/typescript/src/navigation/expo-router/index.d.ts +37 -0
  146. package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -0
  147. package/lib/typescript/src/navigation/expo-router/types.d.ts +44 -0
  148. package/lib/typescript/src/navigation/expo-router/types.d.ts.map +1 -0
  149. package/lib/typescript/src/navigation/index.d.ts +42 -3
  150. package/lib/typescript/src/navigation/index.d.ts.map +1 -1
  151. package/lib/typescript/src/navigation/navigator.d.ts +7 -0
  152. package/lib/typescript/src/navigation/navigator.d.ts.map +1 -0
  153. package/lib/typescript/src/navigation/screen/ReanimatedTrueSheetScreen.d.ts +1 -1
  154. package/lib/typescript/src/navigation/screen/ReanimatedTrueSheetScreen.d.ts.map +1 -1
  155. package/lib/typescript/src/navigation/screen/TrueSheetScreen.d.ts +1 -1
  156. package/lib/typescript/src/navigation/screen/TrueSheetScreen.d.ts.map +1 -1
  157. package/lib/typescript/src/navigation/screen/types.d.ts +3 -4
  158. package/lib/typescript/src/navigation/screen/types.d.ts.map +1 -1
  159. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +5 -6
  160. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -1
  161. package/lib/typescript/src/navigation/types.d.ts +65 -5
  162. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  163. package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts +1 -1
  164. package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts.map +1 -1
  165. package/lib/typescript/src/web/dom.d.ts +7 -0
  166. package/lib/typescript/src/web/dom.d.ts.map +1 -0
  167. package/lib/typescript/src/web/vaul/constants.d.ts +1 -0
  168. package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -1
  169. package/lib/typescript/src/web/vaul/context.d.ts +1 -0
  170. package/lib/typescript/src/web/vaul/context.d.ts.map +1 -1
  171. package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
  172. package/lib/typescript/src/web/vaul/index.d.ts +7 -1
  173. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
  174. package/package.json +32 -9
  175. package/react-native.config.js +1 -0
  176. package/src/TrueSheet.tsx +184 -46
  177. package/src/TrueSheet.types.ts +77 -1
  178. package/src/TrueSheet.web.tsx +135 -64
  179. package/src/TrueSheetOverlay.tsx +24 -0
  180. package/src/TrueSheetOverlay.web.tsx +33 -0
  181. package/src/TrueSheetPeek.web.tsx +4 -3
  182. package/src/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  183. package/src/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  184. package/src/fabric/TrueSheetViewNativeComponent.ts +5 -0
  185. package/src/index.ts +1 -0
  186. package/src/mocks/index.ts +7 -0
  187. package/src/mocks/navigation.ts +32 -25
  188. package/src/mocks/navigationExpoRouter.ts +41 -0
  189. package/src/mocks/reanimated.ts +8 -9
  190. package/src/navigation/{TrueSheetView.tsx → TrueSheetNavigatorContent.tsx} +24 -17
  191. package/src/navigation/actions.ts +78 -0
  192. package/src/navigation/base-types.ts +20 -0
  193. package/src/navigation/createTrueSheetRouter.ts +194 -0
  194. package/src/navigation/expo-router/base-types.ts +18 -0
  195. package/src/navigation/expo-router/index.ts +90 -0
  196. package/src/navigation/expo-router/types.ts +116 -0
  197. package/src/navigation/index.ts +68 -2
  198. package/src/navigation/navigator.ts +18 -0
  199. package/src/navigation/screen/ReanimatedTrueSheetScreen.tsx +2 -2
  200. package/src/navigation/screen/TrueSheetScreen.tsx +2 -2
  201. package/src/navigation/screen/types.ts +4 -6
  202. package/src/navigation/screen/useSheetScreenState.ts +10 -15
  203. package/src/navigation/types.ts +99 -16
  204. package/src/navigation/useTrueSheetNavigation.ts +1 -1
  205. package/src/web/dom.ts +9 -0
  206. package/src/web/vaul/constants.ts +4 -0
  207. package/src/web/vaul/context.ts +1 -0
  208. package/src/web/vaul/helpers.ts +13 -6
  209. package/src/web/vaul/index.tsx +46 -1
  210. package/lib/module/navigation/TrueSheetRouter.js.map +0 -1
  211. package/lib/module/navigation/TrueSheetView.js.map +0 -1
  212. package/lib/module/navigation/createTrueSheetNavigator.js +0 -63
  213. package/lib/module/navigation/createTrueSheetNavigator.js.map +0 -1
  214. package/lib/typescript/src/navigation/TrueSheetRouter.d.ts +0 -57
  215. package/lib/typescript/src/navigation/TrueSheetRouter.d.ts.map +0 -1
  216. package/lib/typescript/src/navigation/TrueSheetView.d.ts +0 -10
  217. package/lib/typescript/src/navigation/TrueSheetView.d.ts.map +0 -1
  218. package/lib/typescript/src/navigation/createTrueSheetNavigator.d.ts +0 -35
  219. package/lib/typescript/src/navigation/createTrueSheetNavigator.d.ts.map +0 -1
  220. package/src/navigation/TrueSheetRouter.ts +0 -234
  221. package/src/navigation/createTrueSheetNavigator.tsx +0 -93
@@ -1,4 +1,4 @@
1
- import type { ComponentType, ReactElement } from 'react';
1
+ import type { Component, ComponentType, ReactElement, RefObject } from 'react';
2
2
  import type {
3
3
  ColorValue,
4
4
  NativeSyntheticEvent,
@@ -36,6 +36,7 @@ export type WillPresentEvent = NativeSyntheticEvent<DetentInfoEventPayload>;
36
36
  export type DidPresentEvent = NativeSyntheticEvent<DetentInfoEventPayload>;
37
37
  export type WillDismissEvent = NativeSyntheticEvent<null>;
38
38
  export type DidDismissEvent = NativeSyntheticEvent<null>;
39
+ export type DismissAttemptEvent = NativeSyntheticEvent<null>;
39
40
  export type DragBeginEvent = NativeSyntheticEvent<DetentInfoEventPayload>;
40
41
  export type DragChangeEvent = NativeSyntheticEvent<DetentInfoEventPayload>;
41
42
  export type DragEndEvent = NativeSyntheticEvent<DetentInfoEventPayload>;
@@ -165,6 +166,28 @@ export type ScrollEdgeEffect = 'automatic' | 'hard' | 'soft' | 'hidden';
165
166
  * Options for scrollable behavior.
166
167
  */
167
168
  export interface ScrollableOptions {
169
+ /**
170
+ * Applies the bottom safe-area inset to the scroll content automatically
171
+ * while the content can scroll, so the last item clears the home indicator
172
+ * (iOS) and navigation bar (Android).
173
+ *
174
+ * - **iOS**: Sets the scroll view's native
175
+ * [`contentInsetAdjustmentBehavior`](https://developer.apple.com/documentation/uikit/uiscrollview/contentinsetadjustmentbehavior)
176
+ * to `automatic` while it's plugged into the sheet — no need to set React
177
+ * Native's iOS-only prop yourself.
178
+ * - **Android**: Pads the scroll content with the bottom window inset,
179
+ * mirroring iOS's `automatic` behavior — parity React Native's iOS-only
180
+ * prop can't provide.
181
+ * - **Web**: Lifts the content above the safe area
182
+ * (`env(safe-area-inset-bottom)`).
183
+ *
184
+ * Only applies while the sheet's `insetAdjustment` is `'automatic'`. When
185
+ * the sheet has a relative footer, the footer absorbs the inset instead.
186
+ *
187
+ * @default true
188
+ */
189
+ contentInsetAdjustmentBehavior?: boolean;
190
+
168
191
  /**
169
192
  * Extra offset when scrolling to the focused input when keyboard appears.
170
193
  *
@@ -172,6 +195,18 @@ export interface ScrollableOptions {
172
195
  */
173
196
  keyboardScrollOffset?: number;
174
197
 
198
+ /**
199
+ * Adjusts the bottom inset applied to the scrollable when the keyboard is shown.
200
+ * Positive values increase the inset; negative values reduce it.
201
+ * Pass `-insets.bottom` to cancel out safe-area padding already included in
202
+ * the content's `paddingBottom`.
203
+ * Does not affect the auto scroll of the focused input — it always targets
204
+ * the keyboard edge.
205
+ *
206
+ * @default 0
207
+ */
208
+ keyboardOffset?: number;
209
+
175
210
  /**
176
211
  * Whether scrolling the content expands the sheet to the next detent.
177
212
  * When `false`, only the grabber can expand the sheet.
@@ -397,6 +432,21 @@ export interface TrueSheetProps extends ViewProps {
397
432
  */
398
433
  initialDetentAnimated?: boolean;
399
434
 
435
+ /**
436
+ * Specify whether the sheet content should mount lazily, on first presentation.
437
+ * Set to `false` to mount the content before presentation without presenting the sheet.
438
+ * Use this when content is not ready on the first render, then call `present()`
439
+ * after your readiness signal so auto detents measure the settled content.
440
+ *
441
+ * Content that requires window attachment to measure, such as SwiftUI-hosted views,
442
+ * still only measures during presentation.
443
+ *
444
+ * @platform android
445
+ * @platform ios
446
+ * @default true
447
+ */
448
+ lazy?: boolean;
449
+
400
450
  /**
401
451
  * The detent index that the sheet should start to dim the background.
402
452
  * This is ignored if `dimmed` is set to `false`.
@@ -407,6 +457,7 @@ export interface TrueSheetProps extends ViewProps {
407
457
 
408
458
  /**
409
459
  * Prevents interactive dismissal of the Sheet.
460
+ * Blocked attempts fire `onDismissAttempt`, so you can confirm before dismissing programmatically.
410
461
  *
411
462
  * @default true
412
463
  */
@@ -568,8 +619,26 @@ export interface TrueSheetProps extends ViewProps {
568
619
  */
569
620
  footerStyle?: StyleProp<ViewStyle>;
570
621
 
622
+ /**
623
+ * A ref to the scrollable component (e.g. `ScrollView`, `FlatList`)
624
+ * rendered within the sheet content.
625
+ * Required for scrollable handling — nested scrolling, keyboard insets,
626
+ * and `auto` detent sizing are wired to this scrollable.
627
+ *
628
+ * Example:
629
+ * ```tsx
630
+ * const scrollableRef = useRef<ScrollView>(null)
631
+ *
632
+ * <TrueSheet scrollableRef={scrollableRef}>
633
+ * <ScrollView ref={scrollableRef} />
634
+ * </TrueSheet>
635
+ * ```
636
+ */
637
+ scrollableRef?: RefObject<Component<unknown> | null>;
638
+
571
639
  /**
572
640
  * Options for scrollable behavior.
641
+ * Applies to the scrollable provided via `scrollableRef`.
573
642
  */
574
643
  scrollableOptions?: ScrollableOptions;
575
644
 
@@ -621,6 +690,13 @@ export interface TrueSheetProps extends ViewProps {
621
690
  */
622
691
  onDidDismiss?: (event: DidDismissEvent) => void;
623
692
 
693
+ /**
694
+ * Called when the user tries to dismiss the sheet while `dismissible` is `false`,
695
+ * e.g. by dragging down or pressing back/escape. Tapping the dim does not count.
696
+ * The sheet stays presented. Confirm with the user, then call `dismiss()` to proceed.
697
+ */
698
+ onDismissAttempt?: (event: DismissAttemptEvent) => void;
699
+
624
700
  /**
625
701
  * Called when the detent of the sheet has changed.
626
702
  * Either by dragging or programatically.
@@ -10,7 +10,7 @@ import {
10
10
  useRef,
11
11
  useState,
12
12
  } from 'react';
13
- import type { LayoutChangeEvent, ViewStyle } from 'react-native';
13
+ import type { DimensionValue, LayoutChangeEvent } from 'react-native';
14
14
  import { StyleSheet, useColorScheme, useWindowDimensions, View } from 'react-native';
15
15
 
16
16
  import type {
@@ -31,6 +31,7 @@ import type {
31
31
  TrueSheetStaticMethods,
32
32
  WillBlurEvent,
33
33
  WillDismissEvent,
34
+ DismissAttemptEvent,
34
35
  WillFocusEvent,
35
36
  WillPresentEvent,
36
37
  } from './TrueSheet.types';
@@ -51,18 +52,19 @@ import {
51
52
  DEFAULT_GRABBER_WIDTH,
52
53
  DEFAULT_MAX_WIDTH,
53
54
  } from './web/constants';
55
+ import { getDOMElement } from './web/dom';
54
56
  import { Drawer } from './web/vaul';
55
- import { DEFAULT_PEEK_HEIGHT, TRANSITIONS } from './web/vaul/constants';
56
-
57
- // First vertically scrollable descendant — web mirror of native's
58
- // findScrollView (RN-web ScrollView/FlatList render a node with
59
- // `overflow-y: auto`; its first child is the content container).
60
- const findVerticalScroller = (root: HTMLElement): HTMLElement | null => {
61
- for (const el of root.querySelectorAll<HTMLElement>('*')) {
62
- const { overflowY } = window.getComputedStyle(el);
63
- if (overflowY === 'auto' || overflowY === 'scroll') return el;
64
- }
65
- return null;
57
+ import { DEFAULT_PEEK_HEIGHT, DRAG_CLASS, TRANSITIONS } from './web/vaul/constants';
58
+
59
+ // Resolves the user's `scrollableRef` to its scrollable DOM element RN-web
60
+ // ScrollView refs expose the scroll node directly (with helpers attached);
61
+ // list refs (e.g. FlatList) expose it via getScrollableNode(). The node's
62
+ // first child is the content container.
63
+ const getScrollableElement = (scrollable: unknown): HTMLElement | null => {
64
+ if (!scrollable) return null;
65
+ if (scrollable instanceof HTMLElement) return scrollable;
66
+ const node = (scrollable as { getScrollableNode?: () => unknown }).getScrollableNode?.();
67
+ return node instanceof HTMLElement ? node : null;
66
68
  };
67
69
 
68
70
  const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props, ref) => {
@@ -78,6 +80,8 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
78
80
  maxContentWidth,
79
81
  anchor = 'center',
80
82
  anchorOffset = DEFAULT_ANCHOR_OFFSET,
83
+ scrollableRef,
84
+ scrollableOptions,
81
85
  grabber = true,
82
86
  grabberOptions,
83
87
  accessibilityOptions,
@@ -102,6 +106,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
102
106
  onDidPresent,
103
107
  onWillDismiss,
104
108
  onDidDismiss,
109
+ onDismissAttempt,
105
110
  onDetentChange,
106
111
  onDragBegin,
107
112
  onDragChange,
@@ -195,19 +200,34 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
195
200
  e.preventDefault();
196
201
  return;
197
202
  }
203
+ if (!(target instanceof Element)) return;
204
+ const wrapper = drawerContentRef.current?.closest('[data-vaul-detached-wrapper]');
205
+ if (!wrapper) return;
198
206
  // The footer is rendered via vaul's `detachedSiblings` as a sibling of
199
207
  // Drawer.Content inside [data-vaul-detached-wrapper], so Radix treats
200
208
  // clicks on it as "outside" the content. Don't dismiss for clicks that
201
209
  // landed inside the wrapper.
202
- if (target instanceof Element) {
203
- const wrapper = drawerContentRef.current?.closest('[data-vaul-detached-wrapper]');
204
- if (wrapper && wrapper.contains(target)) {
205
- e.preventDefault();
206
- }
210
+ if (wrapper.contains(target)) {
211
+ e.preventDefault();
212
+ return;
213
+ }
214
+ // Every sheet runs Radix non-modal and portals its overlay + wrapper as
215
+ // siblings into the shared container, so a press on a sheet stacked above
216
+ // this one (its content or its dim overlay) is also delivered here as
217
+ // "outside". Sheets present in document order, so anything after our
218
+ // wrapper belongs to a descendant. Radix may dispatch on the deferred
219
+ // click (touch), after a dismissing descendant has left the DOM — treat
220
+ // disconnected as above too.
221
+ /* eslint-disable no-bitwise */
222
+ const above = Node.DOCUMENT_POSITION_FOLLOWING | Node.DOCUMENT_POSITION_DISCONNECTED;
223
+ if (wrapper.compareDocumentPosition(target) & above) {
224
+ e.preventDefault();
207
225
  }
226
+ /* eslint-enable no-bitwise */
208
227
  };
209
228
 
210
229
  const dismissAboveRef = useRef<(animated?: boolean) => Promise<void>>(async () => {});
230
+ const dismissDescendantsRef = useRef<() => void>(() => {});
211
231
 
212
232
  const methods = useMemo<TrueSheetMethods>(
213
233
  () => ({
@@ -274,6 +294,17 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
274
294
  const peekElRef = useRef<View>(null);
275
295
  const peekContext = useMemo(() => ({ contentRef, peekRef: peekElRef, setPeekContentHeight }), []);
276
296
 
297
+ // The peek's own onLayout only fires when *it* resizes. Content added above
298
+ // it moves it without resizing it, so re-measure its offset whenever the
299
+ // content view's layout changes.
300
+ const handleContentLayout = useCallback(() => {
301
+ const peekEl = getDOMElement(peekElRef.current);
302
+ const contentEl = getDOMElement(contentRef.current);
303
+ if (peekEl && contentEl) {
304
+ setPeekContentHeight(measurePeekContentHeight(peekEl, contentEl));
305
+ }
306
+ }, []);
307
+
277
308
  // An absolute (floating) header overlaps the content, so it contributes no
278
309
  // height to the 'auto' detent measurement.
279
310
  const absoluteHeader = headerOptions?.position === 'absolute';
@@ -302,8 +333,9 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
302
333
  return [
303
334
  footerStyle,
304
335
  {
305
- paddingBottom: `calc(${baseCss} + env(safe-area-inset-bottom, 0px))`,
306
- } as unknown as ViewStyle,
336
+ // RN types don't model CSS calc(), but RN-web passes it through
337
+ paddingBottom: `calc(${baseCss} + env(safe-area-inset-bottom, 0px))` as DimensionValue,
338
+ },
307
339
  ];
308
340
  }, [footerOwnsInset, footerStyle]);
309
341
 
@@ -321,23 +353,19 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
321
353
  // `naturalHeight`.
322
354
  const [hasBoundedScrollable, setHasBoundedScrollable] = useState(false);
323
355
  const [scrollableAutoHeight, setScrollableAutoHeight] = useState(0);
324
- const pinnedScrollerRef = useRef<HTMLElement | null>(null);
356
+ const detectedScrollerRef = useRef<HTMLElement | null>(null);
325
357
 
326
- // Natural content height (header + content + footer, with a pinned
358
+ // Natural content height (header + content + footer, with a detected
327
359
  // scrollable's viewport replaced by its content size) — the height the
328
360
  // content wants regardless of the sheet's bounds.
329
361
  const measureNaturalHeight = useCallback(() => {
330
- const contentEl = contentRef.current as unknown as HTMLElement | null;
362
+ const contentEl = getDOMElement(contentRef.current);
331
363
  if (!contentEl || !contentEl.isConnected) return 0;
332
- const headerEl = absoluteHeaderRef.current
333
- ? null
334
- : (headerElRef.current as unknown as HTMLElement | null);
335
- const footerEl = absoluteFooterRef.current
336
- ? null
337
- : (footerElRef.current as unknown as HTMLElement | null);
364
+ const headerEl = absoluteHeaderRef.current ? null : getDOMElement(headerElRef.current);
365
+ const footerEl = absoluteFooterRef.current ? null : getDOMElement(footerElRef.current);
338
366
  let height =
339
367
  (headerEl?.offsetHeight ?? 0) + (footerEl?.offsetHeight ?? 0) + contentEl.offsetHeight;
340
- const scroller = pinnedScrollerRef.current;
368
+ const scroller = detectedScrollerRef.current;
341
369
  const scrollContent = scroller?.firstElementChild;
342
370
  if (scroller?.isConnected && scrollContent instanceof HTMLElement) {
343
371
  height += scrollContent.offsetHeight - scroller.clientHeight;
@@ -371,20 +399,18 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
371
399
  scrollContent: Element | null;
372
400
  } | null = null;
373
401
 
374
- // (Re)pin the first vertical scrollable and observe the nodes whose size
402
+ // (Re)resolve the plugged scrollable and observe the nodes whose size
375
403
  // feeds the natural height — content growth inside a bounded scroller is
376
404
  // invisible to the sheet's own layout (mirrors native's contentSize
377
405
  // observation). Resolves the content node live: the layout-branch swap
378
406
  // (natural flow ↔ sized) remounts it.
379
407
  const observe = () => {
380
- const contentEl = contentRef.current as unknown as HTMLElement | null;
381
- const headerEl = headerElRef.current as unknown as HTMLElement | null;
408
+ const contentEl = getDOMElement(contentRef.current);
409
+ const headerEl = getDOMElement(headerElRef.current);
382
410
 
383
411
  // Skip mutations that don't change the observed topology — e.g. a
384
- // virtualized list mounting rows inside the pinned scroller, which
385
- // would otherwise trigger a full-subtree rescan (findVerticalScroller
386
- // resolves computed styles) on every scroll frame. Size changes are
387
- // already covered by the ResizeObserver.
412
+ // virtualized list mounting rows inside the plugged scroller. Size
413
+ // changes are already covered by the ResizeObserver.
388
414
  if (
389
415
  observed &&
390
416
  observed.contentEl === contentEl &&
@@ -396,9 +422,10 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
396
422
  return;
397
423
  }
398
424
 
399
- const scroller =
400
- contentEl && contentEl.isConnected ? findVerticalScroller(contentEl) : null;
401
- pinnedScrollerRef.current = scroller;
425
+ const resolved =
426
+ contentEl && contentEl.isConnected ? getScrollableElement(scrollableRef?.current) : null;
427
+ const scroller = resolved?.isConnected ? resolved : null;
428
+ detectedScrollerRef.current = scroller;
402
429
  setHasBoundedScrollable(scroller != null);
403
430
  observed = scroller
404
431
  ? { contentEl, headerEl, scroller, scrollContent: scroller.firstElementChild }
@@ -431,7 +458,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
431
458
  mutationObserver?.disconnect();
432
459
  resizeObserver?.disconnect();
433
460
  };
434
- }, [isOpen, hasAutoDetent, measureNaturalHeight]);
461
+ }, [isOpen, hasAutoDetent, measureNaturalHeight, scrollableRef]);
435
462
 
436
463
  // Below the last detent a vertical touch pan moves the sheet, not the
437
464
  // content — `[data-vaul-scroll-locked]` disables vertical touch panning on
@@ -549,13 +576,11 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
549
576
  // measure either: they fire while the portal subtree is still detached.
550
577
  // Geometry only runs while the drawer is mounted, so the elements are
551
578
  // measurable here; fall back to the state value when they're absent.
552
- const headerEl = headerElRef.current as unknown as HTMLElement | null;
579
+ const headerEl = getDOMElement(headerElRef.current);
553
580
  // A relative footer is pushed off-screen at the peek detent — excluded
554
- const footerEl = absoluteFooterRef.current
555
- ? (footerElRef.current as unknown as HTMLElement | null)
556
- : null;
557
- const peekEl = peekElRef.current as unknown as HTMLElement | null;
558
- const contentEl = contentRef.current as unknown as HTMLElement | null;
581
+ const footerEl = absoluteFooterRef.current ? getDOMElement(footerElRef.current) : null;
582
+ const peekEl = getDOMElement(peekElRef.current);
583
+ const contentEl = getDOMElement(contentRef.current);
559
584
  const livePeekContentHeight =
560
585
  peekEl && contentEl ? measurePeekContentHeight(peekEl, contentEl) : inputs.peekContentHeight;
561
586
  const livePeekHeight =
@@ -730,6 +755,9 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
730
755
 
731
756
  const present = isOpen;
732
757
  if (!present) {
758
+ // Mirror native: dismissing a sheet takes every sheet stacked above it
759
+ // (iOS dismisses from the presenter; Android runs dismissStack first).
760
+ dismissDescendantsRef.current();
733
761
  // Pair willBlur with willDismiss on dismiss — mirrors native iOS
734
762
  // emitWillDismissEvents (blur fires before dismiss). willPresent is
735
763
  // deferred to `start()` below (needs the mounted drawer's geometry).
@@ -847,6 +875,13 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
847
875
  isFormSheet
848
876
  );
849
877
  dismissAboveRef.current = dismissAbove;
878
+ // Captured per render: by the time the dismiss effect runs, this sheet has
879
+ // already been popped from the stack, so `dismissAbove` can't find it.
880
+ dismissDescendantsRef.current = () => {
881
+ for (let i = descendants.length - 1; i >= 0; i--) {
882
+ descendants[i]!.ref.current?.dismiss();
883
+ }
884
+ };
850
885
 
851
886
  // Mirror Android: translate this sheet down to match the deepest descendant's
852
887
  // top so the whole stack visually aligns. Cascades because every ancestor
@@ -892,17 +927,23 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
892
927
  return;
893
928
  }
894
929
 
895
- // Track only the immediate child's snap point. Walking deeper descendants
896
- // would push this sheet further when a grandchild opens, even when our
897
- // own child didn't move (e.g., child skipped its cascade for a page
898
- // grandchild) leaving a visible gap between this sheet and its child.
930
+ // Track the immediate child's *actual* top: its cascade transform when it
931
+ // was itself pushed behind a grandchild, else its own snap point. Mirrors
932
+ // Android's addTranslation propagating extra translation up the stack
933
+ // a grandchild opening pushes this sheet only as far as our child moved.
934
+ const readTranslateY = (el: HTMLElement) => {
935
+ const match = el.style.transform.match(/translate3d\([^,]*,\s*(-?\d*\.?\d+)px/);
936
+ return match ? Number.parseFloat(match[1]!) : null;
937
+ };
899
938
  const computeTargetY = () => {
900
939
  const parentSnap =
901
940
  Number.parseFloat(parent.style.getPropertyValue('--snap-point-height')) || 0;
902
941
  const node = descendants[0]?.nodeRef.current;
903
942
  if (!node) return parentSnap;
904
- const childSnap = Number.parseFloat(node.style.getPropertyValue('--snap-point-height')) || 0;
905
- return Math.max(parentSnap, childSnap);
943
+ const childTop =
944
+ readTranslateY(node) ??
945
+ (Number.parseFloat(node.style.getPropertyValue('--snap-point-height')) || 0);
946
+ return Math.max(parentSnap, childTop);
906
947
  };
907
948
 
908
949
  // When a form-sheet parent has a form-sheet descendant, clip the parent to
@@ -938,18 +979,24 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
938
979
 
939
980
  const apply = () => {
940
981
  applyFormClip();
941
- // Mirror iOS: a page-sheet child fully covers a form-sheet parent, so the
942
- // cascade push-down has no visible effect and would briefly peek above
943
- // the page during the present animation. Leave the parent put.
944
- const child = descendants[0];
945
- if (isFormSheet && child && !child.isFormSheetRef.current) {
982
+ // Mirror iPad/Android (`isExpanded`): a parent at full height (detent 1)
983
+ // stays put and the child simply overlays it. Full height is the sheet's
984
+ // own ceiling the viewport for a page sheet, the capped content-fit
985
+ // height for a form sheet — so compare against where detent 1 lands.
986
+ const parentSnap =
987
+ Number.parseFloat(parent.style.getPropertyValue('--snap-point-height')) || 0;
988
+ const { effectiveH, ceiling } = computeDetentGeometry();
989
+ if (parentSnap <= effectiveH - ceiling + 0.5) {
946
990
  parent.style.transition = transition;
947
991
  parent.style.transform = '';
948
992
  return;
949
993
  }
994
+ // Mirror Android: stay put while the child is being dragged (its live
995
+ // transform would drag us along during drag-to-dismiss). The release
996
+ // snap rewrites the child's style and re-applies.
997
+ if (descendants[0]?.nodeRef.current?.classList.contains(DRAG_CLASS)) return;
950
998
  const targetY = computeTargetY();
951
- const match = parent.style.transform.match(/translate3d\([^,]*,\s*(-?\d*\.?\d+)px/);
952
- const currentY = match ? Number.parseFloat(match[1]!) : 0;
999
+ const currentY = readTranslateY(parent) ?? 0;
953
1000
  if (Math.abs(currentY - targetY) < 0.5) return;
954
1001
  parent.style.transition = transition;
955
1002
  parent.style.transform = `translate3d(0, ${targetY}px, 0)`;
@@ -961,12 +1008,15 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
961
1008
  // inline style changes.
962
1009
  const observer = new MutationObserver(apply);
963
1010
  observer.observe(parent, { attributes: true, attributeFilter: ['style'] });
1011
+ // The child's own cascade lands in a later frame — follow it when it moves.
1012
+ const childNode = descendants[0]?.nodeRef.current;
1013
+ if (childNode) observer.observe(childNode, { attributes: true, attributeFilter: ['style'] });
964
1014
 
965
1015
  return () => {
966
1016
  cancelAnimationFrame(raf);
967
1017
  observer.disconnect();
968
1018
  };
969
- }, [descendants, activeSnapPoint, cornerRadius, isFormSheet, isOpen]);
1019
+ }, [descendants, activeSnapPoint, cornerRadius, isFormSheet, isOpen, computeDetentGeometry]);
970
1020
 
971
1021
  // Focus/blur events fire when a descendant sheet appears on top of this one
972
1022
  // (blur) or when all descendants are dismissed (focus). will-events fire
@@ -977,12 +1027,17 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
977
1027
  const onDidBlurRef = useRef(onDidBlur);
978
1028
  const onWillFocusRef = useRef(onWillFocus);
979
1029
  const onDidFocusRef = useRef(onDidFocus);
1030
+ const onDismissAttemptRef = useRef(onDismissAttempt);
980
1031
  useEffect(() => {
981
1032
  onWillBlurRef.current = onWillBlur;
982
1033
  onDidBlurRef.current = onDidBlur;
983
1034
  onWillFocusRef.current = onWillFocus;
984
1035
  onDidFocusRef.current = onDidFocus;
1036
+ onDismissAttemptRef.current = onDismissAttempt;
985
1037
  });
1038
+ const handleDismissAttempt = useCallback(() => {
1039
+ onDismissAttemptRef.current?.({ nativeEvent: null } as DismissAttemptEvent);
1040
+ }, []);
986
1041
 
987
1042
  const prevDescendantCountRef = useRef(0);
988
1043
  useEffect(() => {
@@ -1073,12 +1128,25 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
1073
1128
  // Lift content above iOS home indicator / bottom safe area when enabled.
1074
1129
  // A relative footer owns the inset instead (see resolvedFooterStyle); an
1075
1130
  // absolute footer floats over the content, so the content keeps its lift.
1131
+ // A plugged scrollable keeps the lift too (contentInsetAdjustmentBehavior,
1132
+ // default on) — disable it to scroll edge-to-edge behind the indicator
1133
+ // with user-applied content padding.
1076
1134
  paddingBottom:
1077
- insetAdjustment === 'automatic' && !(footerOwnsInset && !absoluteFooter)
1135
+ insetAdjustment === 'automatic' &&
1136
+ !(footerOwnsInset && !absoluteFooter) &&
1137
+ (!scrollableRef || (scrollableOptions?.contentInsetAdjustmentBehavior ?? true))
1078
1138
  ? 'env(safe-area-inset-bottom, 0px)'
1079
1139
  : 0,
1080
1140
  }),
1081
- [backgroundColor, effectiveCornerRadius, insetAdjustment, footerOwnsInset, absoluteFooter]
1141
+ [
1142
+ backgroundColor,
1143
+ effectiveCornerRadius,
1144
+ insetAdjustment,
1145
+ footerOwnsInset,
1146
+ absoluteFooter,
1147
+ scrollableRef,
1148
+ scrollableOptions,
1149
+ ]
1082
1150
  );
1083
1151
 
1084
1152
  const defaultGrabberColor =
@@ -1195,6 +1263,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
1195
1263
  onDrag={handleDrag}
1196
1264
  onRelease={handleRelease}
1197
1265
  dismissible={dismissible}
1266
+ onDismissAttempt={handleDismissAttempt}
1198
1267
  draggable={draggable}
1199
1268
  repositionInputs={false}
1200
1269
  modal={dimmed}
@@ -1258,10 +1327,12 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
1258
1327
  {/* Content lays out naturally like native — fill only for auto
1259
1328
  detents with a plugged scrollable, where natural layout is
1260
1329
  circular (sheet height derives from the scroll content size).
1261
- Mirrors the native shadow node's scrollableBounded behavior. */}
1330
+ Unlike native, CSS won't cap the content to the container
1331
+ (no fit-content flex basis), so the fill is still needed here. */}
1262
1332
  <View
1263
1333
  ref={contentRef}
1264
1334
  style={hasAutoDetent && hasBoundedScrollable ? [contentFillStyle, style] : style}
1335
+ onLayout={handleContentLayout}
1265
1336
  >
1266
1337
  {children}
1267
1338
  </View>
@@ -1284,7 +1355,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
1284
1355
  {isValidElement(header) ? header : createElement(header)}
1285
1356
  </View>
1286
1357
  )}
1287
- <View ref={contentRef} style={style}>
1358
+ <View ref={contentRef} style={style} onLayout={handleContentLayout}>
1288
1359
  {children}
1289
1360
  </View>
1290
1361
  {footer && !absoluteFooter && (
@@ -0,0 +1,24 @@
1
+ import { StyleSheet, type ViewProps } from 'react-native';
2
+
3
+ import TrueSheetOverlayViewNativeComponent from './fabric/TrueSheetOverlayViewNativeComponent';
4
+
5
+ /**
6
+ * Renders its children in a native layer above every presented sheet — for
7
+ * toasts, dialogs, and other content a sheet must not cover.
8
+ * Fills the window; touches that miss its children pass through to the views beneath.
9
+ */
10
+ export const TrueSheetOverlay = ({ style, ...rest }: ViewProps) => (
11
+ <TrueSheetOverlayViewNativeComponent {...rest} style={[styles.overlay, style]} />
12
+ );
13
+
14
+ const styles = StyleSheet.create({
15
+ // Sized to the window from native — absolute so it takes no layout space.
16
+ // Children render in a native container, so the host itself must never
17
+ // catch touches (Android forces it visible on layout).
18
+ overlay: {
19
+ position: 'absolute',
20
+ top: 0,
21
+ left: 0,
22
+ pointerEvents: 'none',
23
+ },
24
+ });
@@ -0,0 +1,33 @@
1
+ /// <reference lib="dom" />
2
+ import { useLayoutEffect, useState } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { StyleSheet, View, type ViewProps } from 'react-native';
5
+
6
+ /**
7
+ * Renders its children in a layer above every presented sheet — for
8
+ * toasts, dialogs, and other content a sheet must not cover.
9
+ * Fills the window; touches that miss its children pass through to the views beneath.
10
+ */
11
+ export const TrueSheetOverlay = ({ style, ...rest }: ViewProps) => {
12
+ // Portal target appended to the body so it stacks above the sheets' portal
13
+ const [container] = useState(() =>
14
+ typeof document !== 'undefined' ? document.createElement('div') : null
15
+ );
16
+
17
+ useLayoutEffect(() => {
18
+ if (!container) return;
19
+
20
+ container.style.cssText = 'position:fixed;inset:0;pointer-events:none';
21
+ document.body.appendChild(container);
22
+ return () => {
23
+ container.remove();
24
+ };
25
+ }, [container]);
26
+
27
+ if (!container) return null;
28
+
29
+ return createPortal(
30
+ <View pointerEvents="box-none" {...rest} style={[StyleSheet.absoluteFill, style]} />,
31
+ container
32
+ );
33
+ };
@@ -1,6 +1,8 @@
1
1
  import { createContext, type RefObject, useContext, useEffect, useRef } from 'react';
2
2
  import { type LayoutChangeEvent, View, type ViewProps } from 'react-native';
3
3
 
4
+ import { getDOMElement } from './web/dom';
5
+
4
6
  /**
5
7
  * Reports the measured peek content height to the owning TrueSheet.
6
8
  * @internal
@@ -44,9 +46,8 @@ export const TrueSheetPeek = ({ onLayout, ...rest }: ViewProps) => {
44
46
 
45
47
  const handleLayout = (event: LayoutChangeEvent) => {
46
48
  if (context) {
47
- // On web, View refs resolve to the underlying DOM elements.
48
- const peekElement = viewRef.current as unknown as HTMLElement | null;
49
- const contentElement = context.contentRef.current as unknown as HTMLElement | null;
49
+ const peekElement = getDOMElement(viewRef.current);
50
+ const contentElement = getDOMElement(context.contentRef.current);
50
51
 
51
52
  const bottom =
52
53
  peekElement && contentElement
@@ -6,7 +6,7 @@ export interface NativeProps extends ViewProps {
6
6
  }
7
7
 
8
8
  // interfaceOnly: shadow node/state/descriptor are custom (common/cpp) so the
9
- // content can be bounded to the container when a ScrollView is pinned
9
+ // content can be bounded to the container when a ScrollView is detected
10
10
  export default codegenNativeComponent<NativeProps>('TrueSheetContentView', {
11
11
  interfaceOnly: true,
12
12
  });
@@ -0,0 +1,12 @@
1
+ import type { ViewProps } from 'react-native';
2
+ import { codegenNativeComponent } from 'react-native';
3
+
4
+ export interface NativeProps extends ViewProps {
5
+ // No props needed - sized to the window from native
6
+ }
7
+
8
+ // interfaceOnly: shadow node/state/descriptor are custom (common/cpp) so the
9
+ // overlay is sized to the window
10
+ export default codegenNativeComponent<NativeProps>('TrueSheetOverlayView', {
11
+ interfaceOnly: true,
12
+ });