@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
@@ -6,9 +6,11 @@ import {
6
6
  useCallback,
7
7
  useEffect,
8
8
  useImperativeHandle,
9
+ useLayoutEffect,
9
10
  useMemo,
10
11
  useRef,
11
12
  useState,
13
+ type RefObject,
12
14
  } from 'react';
13
15
  import type { DimensionValue, LayoutChangeEvent } from 'react-native';
14
16
  import { StyleSheet, useColorScheme, useWindowDimensions, View } from 'react-native';
@@ -31,15 +33,25 @@ import type {
31
33
  TrueSheetStaticMethods,
32
34
  WillBlurEvent,
33
35
  WillDismissEvent,
36
+ DismissAttemptEvent,
34
37
  WillFocusEvent,
35
38
  WillPresentEvent,
36
39
  } from './TrueSheet.types';
37
- import { measurePeekContentHeight, TrueSheetPeekContext } from './TrueSheetPeek.web';
38
- import { usePortalContainer, useRegisterSheet, useSheetStack } from './TrueSheetProvider.web';
40
+ import {
41
+ measurePeekContentHeight,
42
+ TrueSheetPeekContext,
43
+ type TrueSheetPeekContextValue,
44
+ } from './TrueSheetPeek.web';
45
+ import {
46
+ usePortalContainer,
47
+ useRegisterSheet,
48
+ useSheetStack,
49
+ type SheetHandle,
50
+ } from './TrueSheetProvider.web';
39
51
  import {
40
52
  COLOR_SURFACE_CONTAINER_LOW_DARK,
41
53
  COLOR_SURFACE_CONTAINER_LOW_LIGHT,
42
- DEFAULT_ANCHOR_OFFSET,
54
+ DEFAULT_PLACEMENT_OFFSET,
43
55
  DEFAULT_CORNER_RADIUS,
44
56
  DEFAULT_DETACHED_OFFSET,
45
57
  DEFAULT_FORM_SHEET_HEIGHT_RATIO,
@@ -52,18 +64,19 @@ import {
52
64
  DEFAULT_MAX_WIDTH,
53
65
  } from './web/constants';
54
66
  import { getDOMElement } from './web/dom';
67
+ import { TrueSheetBackground } from './TrueSheetBackground';
68
+ import { getDetentBackgroundIndex, normalizeDetentBackground } from './detentBackgrounds';
69
+ import { revealFocusedElement, useKeyboardViewport } from './web/keyboard';
70
+ import { observeSheetLayout } from './web/layout';
71
+ import { resolveScrollable } from './scrollable';
55
72
  import { Drawer } from './web/vaul';
56
- import { DEFAULT_PEEK_HEIGHT, TRANSITIONS } from './web/vaul/constants';
57
-
58
- // First vertically scrollable descendant — web mirror of native's
59
- // findScrollView (RN-web ScrollView/FlatList render a node with
60
- // `overflow-y: auto`; its first child is the content container).
61
- const findVerticalScroller = (root: HTMLElement): HTMLElement | null => {
62
- for (const el of root.querySelectorAll<HTMLElement>('*')) {
63
- const { overflowY } = window.getComputedStyle(el);
64
- if (overflowY === 'auto' || overflowY === 'scroll') return el;
65
- }
66
- return null;
73
+ import { DEFAULT_PEEK_HEIGHT, DRAG_CLASS, TRANSITIONS } from './web/vaul/constants';
74
+
75
+ // Resolves the user's `scrollableRef` to its scrollable DOM element. The
76
+ // node's first child is the content container.
77
+ const getScrollableElement = (scrollable: unknown): HTMLElement | null => {
78
+ const node = resolveScrollable(scrollable);
79
+ return node instanceof HTMLElement ? node : null;
67
80
  };
68
81
 
69
82
  const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props, ref) => {
@@ -72,18 +85,26 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
72
85
  name,
73
86
  dismissible = true,
74
87
  draggable = true,
88
+ dismissThreshold = 'half',
75
89
  cornerRadius,
76
90
  style,
77
91
  backgroundColor: backgroundColorProp,
92
+ detentBackgrounds,
93
+ background,
94
+ backgroundStyle,
78
95
  maxContentHeight,
79
96
  maxContentWidth,
80
- anchor = 'center',
81
- anchorOffset = DEFAULT_ANCHOR_OFFSET,
97
+ placement = 'automatic',
98
+ placementOffset = DEFAULT_PLACEMENT_OFFSET,
99
+ scrollableRef,
100
+ scrollableOptions,
82
101
  grabber = true,
83
102
  grabberOptions,
84
103
  accessibilityOptions,
85
104
  detents = [0.5, 1],
86
105
  dimmed = true,
106
+ dimColor,
107
+ dimOpacity,
87
108
  dimmedDetentIndex = 0,
88
109
  initialDetentIndex = -1,
89
110
  header,
@@ -103,6 +124,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
103
124
  onDidPresent,
104
125
  onWillDismiss,
105
126
  onDidDismiss,
127
+ onDismissAttempt,
106
128
  onDetentChange,
107
129
  onDragBegin,
108
130
  onDragChange,
@@ -143,12 +165,33 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
143
165
  const effectiveDetached = isFormSheet || detached;
144
166
 
145
167
  const colorScheme = useColorScheme();
168
+ const shouldAutoPresent = initialDetentIndex >= 0 && initialDetentIndex < validDetents.length;
169
+ const didInitiallyPresentRef = useRef(shouldAutoPresent);
170
+ const [isOpen, setIsOpen] = useState(shouldAutoPresent);
171
+ const isOpenRef = useRef(isOpen);
172
+ isOpenRef.current = isOpen;
173
+ const keyboardViewport = useKeyboardViewport(isOpen);
174
+ const isPresentingRef = useRef(shouldAutoPresent);
175
+ // Native's lifecycle state — updated synchronously so calls in the same tick see each other
176
+ const phaseRef = useRef<SheetPhase>(shouldAutoPresent ? 'presenting' : 'dismissed');
177
+ const resolvePresentRef = useRef<(() => void) | null>(null);
178
+ const resolveDismissRef = useRef<(() => void) | null>(null);
179
+ // Whether willPresent fired for the current open cycle — a present undone
180
+ // before it fires has nothing to pair a dismiss with.
181
+ const didEmitWillPresentRef = useRef(false);
182
+ // Whether the current open cycle came from initialDetentIndex. Native mounts those
183
+ // eagerly, so a dismiss before willPresent lands after the present instead of cancelling it.
184
+ const isAutoPresentRef = useRef(shouldAutoPresent);
185
+ const shouldDismissOnPresentRef = useRef(false);
186
+ const [displayedBackgroundIndex, setDisplayedBackgroundIndex] = useState(
187
+ shouldAutoPresent ? initialDetentIndex : 0
188
+ );
146
189
  const backgroundColor =
190
+ normalizeDetentBackground(
191
+ detentBackgrounds?.[Math.min(displayedBackgroundIndex, validDetents.length - 1)]
192
+ )?.color ??
147
193
  backgroundColorProp ??
148
194
  (colorScheme === 'dark' ? COLOR_SURFACE_CONTAINER_LOW_DARK : COLOR_SURFACE_CONTAINER_LOW_LIGHT);
149
-
150
- const shouldAutoPresent = initialDetentIndex >= 0 && initialDetentIndex < validDetents.length;
151
- const [isOpen, setIsOpen] = useState(shouldAutoPresent);
152
195
  const [activeSnapPoint, setActiveSnapPoint] = useState<SheetDetent | null>(
153
196
  () => validDetents[shouldAutoPresent ? initialDetentIndex : 0] ?? null
154
197
  );
@@ -164,8 +207,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
164
207
  const validDetentsRef = useRef(validDetents);
165
208
  validDetentsRef.current = validDetents;
166
209
 
167
- const hasAutoDetent = validDetents.includes('auto');
168
-
169
210
  const handleSetActiveSnapPoint = useCallback((snapPoint: number | string | null) => {
170
211
  setActiveSnapPoint(
171
212
  snapPoint == null
@@ -176,13 +217,31 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
176
217
  );
177
218
  }, []);
178
219
 
220
+ // Returns null when there's nothing to dismiss. Resolves on didDismiss.
221
+ const dismissSheet = useCallback((): Promise<void> | null => {
222
+ if (phaseRef.current === 'dismissed' || phaseRef.current === 'dismissing') return null;
223
+ settle(resolvePresentRef);
224
+ if (didEmitWillPresentRef.current) {
225
+ setIsOpen(false);
226
+ } else if (isAutoPresentRef.current) {
227
+ shouldDismissOnPresentRef.current = true;
228
+ } else {
229
+ // Mirror native's lazy-mount cancel: nothing shown yet, nothing to animate out
230
+ setIsOpen(false);
231
+ phaseRef.current = 'dismissed';
232
+ return Promise.resolve();
233
+ }
234
+ phaseRef.current = 'dismissing';
235
+ return new Promise((resolve) => {
236
+ resolveDismissRef.current = resolve;
237
+ });
238
+ }, []);
239
+
179
240
  const handleOpenChange = useCallback(
180
241
  (open: boolean) => {
181
- if (!open && isOpen) {
182
- setIsOpen(false);
183
- }
242
+ if (!open) dismissSheet();
184
243
  },
185
- [isOpen]
244
+ [dismissSheet]
186
245
  );
187
246
 
188
247
  const portalContainer = usePortalContainer();
@@ -196,34 +255,67 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
196
255
  e.preventDefault();
197
256
  return;
198
257
  }
258
+ if (!(target instanceof Element)) return;
259
+ const wrapper = drawerContentRef.current?.closest('[data-vaul-detached-wrapper]');
260
+ if (!wrapper) return;
199
261
  // The footer is rendered via vaul's `detachedSiblings` as a sibling of
200
262
  // Drawer.Content inside [data-vaul-detached-wrapper], so Radix treats
201
263
  // clicks on it as "outside" the content. Don't dismiss for clicks that
202
264
  // landed inside the wrapper.
203
- if (target instanceof Element) {
204
- const wrapper = drawerContentRef.current?.closest('[data-vaul-detached-wrapper]');
205
- if (wrapper && wrapper.contains(target)) {
206
- e.preventDefault();
207
- }
265
+ if (wrapper.contains(target)) {
266
+ e.preventDefault();
267
+ return;
268
+ }
269
+ // Every sheet runs Radix non-modal and portals its overlay + wrapper as
270
+ // siblings into the shared container, so a press on a sheet stacked above
271
+ // this one (its content or its dim overlay) is also delivered here as
272
+ // "outside". Sheets present in document order, so anything after our
273
+ // wrapper belongs to a descendant. Radix may dispatch on the deferred
274
+ // click (touch), after a dismissing descendant has left the DOM — treat
275
+ // disconnected as above too.
276
+ /* eslint-disable no-bitwise */
277
+ const above = Node.DOCUMENT_POSITION_FOLLOWING | Node.DOCUMENT_POSITION_DISCONNECTED;
278
+ if (wrapper.compareDocumentPosition(target) & above) {
279
+ e.preventDefault();
208
280
  }
281
+ /* eslint-enable no-bitwise */
209
282
  };
210
283
 
211
284
  const dismissAboveRef = useRef<(animated?: boolean) => Promise<void>>(async () => {});
285
+ const dismissDescendantsRef = useRef<() => void>(() => {});
286
+
287
+ const presentSheet = useCallback(async (index: number, auto: boolean) => {
288
+ const detent = validDetentsRef.current[index];
289
+ if (detent === undefined) {
290
+ throw new Error(
291
+ `TrueSheet: present index (${index}) is out of bounds. detents array has ${validDetentsRef.current.length} item(s)`
292
+ );
293
+ }
294
+ if (phaseRef.current !== 'dismissed') {
295
+ console.warn(ALREADY_PRESENTED_WARNING);
296
+ return;
297
+ }
298
+ phaseRef.current = 'presenting';
299
+ isAutoPresentRef.current = auto;
300
+ isPresentingRef.current = true;
301
+ setDisplayedBackgroundIndex(index);
302
+ setActiveSnapPoint(detent);
303
+ setIsOpen(true);
304
+ await new Promise<void>((resolve) => {
305
+ resolvePresentRef.current = resolve;
306
+ });
307
+ }, []);
212
308
 
213
309
  const methods = useMemo<TrueSheetMethods>(
214
310
  () => ({
215
- present: async (index = 0) => {
216
- const detent = validDetentsRef.current[index];
217
- if (detent === undefined) {
218
- throw new Error(
219
- `TrueSheet: present index (${index}) is out of bounds. detents array has ${validDetentsRef.current.length} item(s)`
220
- );
221
- }
222
- setActiveSnapPoint(detent);
223
- setIsOpen(true);
224
- },
311
+ present: (index = 0) => presentSheet(index, false),
225
312
  dismiss: async () => {
226
- setIsOpen(false);
313
+ const dismissal = dismissSheet();
314
+ if (!dismissal) {
315
+ console.warn(ALREADY_DISMISSED_WARNING);
316
+ return;
317
+ }
318
+ await dismissal;
227
319
  },
228
320
  resize: async (index) => {
229
321
  const detent = validDetentsRef.current[index];
@@ -235,18 +327,49 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
235
327
  setActiveSnapPoint(detent);
236
328
  },
237
329
  dismissStack: async (animated) => {
330
+ if (phaseRef.current === 'dismissed' || phaseRef.current === 'dismissing') {
331
+ console.warn(ALREADY_DISMISSED_WARNING);
332
+ return;
333
+ }
238
334
  await dismissAboveRef.current(animated);
239
335
  },
240
336
  }),
241
- []
337
+ [presentSheet, dismissSheet]
242
338
  );
243
339
 
244
340
  useImperativeHandle(ref, () => methods, [methods]);
245
341
 
246
- const methodsRef = useRef<TrueSheetMethods | null>(methods);
247
- useRegisterSheet(name, methodsRef);
342
+ useEffect(() => {
343
+ if (!shouldAutoPresent || didInitiallyPresentRef.current) return;
344
+ didInitiallyPresentRef.current = true;
345
+ if (phaseRef.current === 'dismissed') {
346
+ presentSheet(initialDetentIndex, true);
347
+ }
348
+ }, [initialDetentIndex, shouldAutoPresent, presentSheet]);
349
+
350
+ // Unmounted mid-transition: don't leave callers awaiting forever
351
+ useEffect(
352
+ () => () => {
353
+ settle(resolvePresentRef);
354
+ settle(resolveDismissRef);
355
+ },
356
+ []
357
+ );
358
+
359
+ const handleRef = useRef<SheetHandle>({
360
+ methods,
361
+ dismissQuietly: async () => {
362
+ await dismissSheet();
363
+ },
364
+ });
365
+ useRegisterSheet(name, handleRef);
248
366
 
249
367
  const drawerContentRef = useRef<HTMLDivElement | null>(null);
368
+ const [drawerContent, setDrawerContent] = useState<HTMLDivElement | null>(null);
369
+ const handleDrawerRef = useCallback((node: HTMLDivElement | null) => {
370
+ drawerContentRef.current = node;
371
+ setDrawerContent(node);
372
+ }, []);
250
373
 
251
374
  // Measured header/footer heights drive the 'peek' snap point — mirrors
252
375
  // native, where the controller tracks headerHeight/footerHeight.
@@ -272,15 +395,45 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
272
395
  // within the content — measured by the peek against `contentRef`.
273
396
  const [peekContentHeight, setPeekContentHeight] = useState(0);
274
397
  const contentRef = useRef<View>(null);
275
- const peekElRef = useRef<View>(null);
276
- const peekContext = useMemo(() => ({ contentRef, peekRef: peekElRef, setPeekContentHeight }), []);
398
+ const peekElRef = useRef<View | null>(null);
399
+
400
+ const measurePeek = useCallback(() => {
401
+ const peekEl = getDOMElement(peekElRef.current);
402
+ const contentEl = getDOMElement(contentRef.current);
403
+ if (peekEl && contentEl) {
404
+ setPeekContentHeight(measurePeekContentHeight(peekEl, contentEl));
405
+ }
406
+ }, []);
407
+
408
+ // Last-attached wins — during a screen swap the incoming peek mounts before
409
+ // the outgoing one unmounts, so the outgoing detach must not clear it.
410
+ const peekContext = useMemo<TrueSheetPeekContextValue>(
411
+ () => ({
412
+ contentRef,
413
+ attachPeek: (view) => {
414
+ peekElRef.current = view;
415
+ measurePeek();
416
+ },
417
+ detachPeek: (view) => {
418
+ if (peekElRef.current !== view) return;
419
+ peekElRef.current = null;
420
+ setPeekContentHeight(0);
421
+ },
422
+ measurePeek: (view) => {
423
+ if (peekElRef.current === view) measurePeek();
424
+ },
425
+ }),
426
+ [measurePeek]
427
+ );
428
+
429
+ // The peek's own onLayout only fires when *it* resizes. Content added above
430
+ // it moves it without resizing it, so re-measure its offset whenever the
431
+ // content view's layout changes.
432
+ const handleContentLayout = measurePeek;
277
433
 
278
434
  // An absolute (floating) header overlaps the content, so it contributes no
279
435
  // height to the 'auto' detent measurement.
280
436
  const absoluteHeader = headerOptions?.position === 'absolute';
281
- const absoluteHeaderRef = useRef(absoluteHeader);
282
- absoluteHeaderRef.current = absoluteHeader;
283
-
284
437
  const resolvedHeaderStyle = absoluteHeader ? [absoluteHeaderStyle, headerStyle] : headerStyle;
285
438
 
286
439
  // Same for an absolute (floating) footer — rendered via vaul's
@@ -316,120 +469,67 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
316
469
  (footer && absoluteFooter ? footerHeight : 0) +
317
470
  peekContentHeight || DEFAULT_PEEK_HEIGHT;
318
471
 
319
- // Web mirror of native's scrollable handling for 'auto' detents: a plugged
320
- // scrollable keeps the sized (bounded) layout so its viewport is capped to
321
- // the visible sheet and can scroll, while the 'auto' height is measured with
322
- // the viewport replaced by the scrollable's content size — mirrors native
323
- // `naturalHeight`.
324
- const [hasBoundedScrollable, setHasBoundedScrollable] = useState(false);
325
- const [scrollableAutoHeight, setScrollableAutoHeight] = useState(0);
326
- const pinnedScrollerRef = useRef<HTMLElement | null>(null);
327
-
328
- // Natural content height (header + content + footer, with a pinned
329
- // scrollable's viewport replaced by its content size) — the height the
330
- // content wants regardless of the sheet's bounds.
331
- const measureNaturalHeight = useCallback(() => {
332
- const contentEl = getDOMElement(contentRef.current);
333
- if (!contentEl || !contentEl.isConnected) return 0;
334
- const headerEl = absoluteHeaderRef.current ? null : getDOMElement(headerElRef.current);
335
- const footerEl = absoluteFooterRef.current ? null : getDOMElement(footerElRef.current);
336
- let height =
337
- (headerEl?.offsetHeight ?? 0) + (footerEl?.offsetHeight ?? 0) + contentEl.offsetHeight;
338
- const scroller = pinnedScrollerRef.current;
339
- const scrollContent = scroller?.firstElementChild;
340
- if (scroller?.isConnected && scrollContent instanceof HTMLElement) {
341
- height += scrollContent.offsetHeight - scroller.clientHeight;
342
- }
343
- return Math.max(0, height);
344
- }, []);
472
+ const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
473
+ const naturalHeightRef = useRef(0);
474
+ const insetBehavior = scrollableOptions?.contentInsetAdjustment ?? 'automatic';
475
+ const footerInsetAdjustment =
476
+ Boolean(footer) &&
477
+ absoluteFooter &&
478
+ (insetBehavior === 'automatic' || insetBehavior === 'footer');
479
+ const measureContent = validDetents.includes('auto') || isFormSheet;
480
+
481
+ useLayoutEffect(() => {
482
+ const layoutEl = sizedLayoutRef.current;
483
+ if (!isOpen || !drawerContent || !layoutEl?.isConnected) return undefined;
484
+
485
+ setHeaderHeight(getDOMElement(headerElRef.current)?.offsetHeight ?? 0);
486
+ setFooterHeight(getDOMElement(footerElRef.current)?.offsetHeight ?? 0);
487
+ measurePeek();
488
+
489
+ const observer = observeSheetLayout({
490
+ drawer: drawerContent,
491
+ layout: layoutEl,
492
+ getContent: () => getDOMElement(contentRef.current),
493
+ getHeader: () => getDOMElement(headerElRef.current),
494
+ getFooter: () => getDOMElement(footerElRef.current),
495
+ getScrollable: () => getScrollableElement(scrollableRef?.current),
496
+ absoluteHeader,
497
+ absoluteFooter,
498
+ footerInsetAdjustment,
499
+ measureContent,
500
+ onHeightChange: (height) => {
501
+ naturalHeightRef.current = height;
502
+ setMeasuredContentHeight(height);
503
+ },
504
+ });
345
505
 
346
- useEffect(() => {
347
- if (!isOpen || !hasAutoDetent) return undefined;
506
+ return () => observer.disconnect();
507
+ }, [
508
+ isOpen,
509
+ drawerContent,
510
+ measurePeek,
511
+ absoluteHeader,
512
+ absoluteFooter,
513
+ footerInsetAdjustment,
514
+ measureContent,
515
+ scrollableRef,
516
+ ]);
348
517
 
518
+ // Re-reveal the focused field once the sheet settles above the keyboard
519
+ useEffect(() => {
520
+ const drawer = drawerContentRef.current;
521
+ if (!keyboardViewport || !drawer) return undefined;
349
522
  let canceled = false;
350
- let rafId = 0;
351
- let mutationObserver: MutationObserver | null = null;
352
- let resizeObserver: ResizeObserver | null = null;
353
-
354
- const attach = () => {
355
- if (canceled) return;
356
- const drawerEl = drawerContentRef.current;
357
- if (!drawerEl || !drawerEl.isConnected) {
358
- // Radix Presence defers the portal mount; poll until the drawer is live.
359
- rafId = window.requestAnimationFrame(attach);
360
- return;
361
- }
362
-
363
- const measure = () => setScrollableAutoHeight(measureNaturalHeight());
364
-
365
- let observed: {
366
- contentEl: HTMLElement | null;
367
- headerEl: HTMLElement | null;
368
- scroller: HTMLElement;
369
- scrollContent: Element | null;
370
- } | null = null;
371
-
372
- // (Re)pin the first vertical scrollable and observe the nodes whose size
373
- // feeds the natural height — content growth inside a bounded scroller is
374
- // invisible to the sheet's own layout (mirrors native's contentSize
375
- // observation). Resolves the content node live: the layout-branch swap
376
- // (natural flow ↔ sized) remounts it.
377
- const observe = () => {
378
- const contentEl = getDOMElement(contentRef.current);
379
- const headerEl = getDOMElement(headerElRef.current);
380
-
381
- // Skip mutations that don't change the observed topology — e.g. a
382
- // virtualized list mounting rows inside the pinned scroller, which
383
- // would otherwise trigger a full-subtree rescan (findVerticalScroller
384
- // resolves computed styles) on every scroll frame. Size changes are
385
- // already covered by the ResizeObserver.
386
- if (
387
- observed &&
388
- observed.contentEl === contentEl &&
389
- contentEl?.isConnected &&
390
- observed.headerEl === headerEl &&
391
- observed.scroller.isConnected &&
392
- observed.scrollContent === observed.scroller.firstElementChild
393
- ) {
394
- return;
395
- }
396
-
397
- const scroller =
398
- contentEl && contentEl.isConnected ? findVerticalScroller(contentEl) : null;
399
- pinnedScrollerRef.current = scroller;
400
- setHasBoundedScrollable(scroller != null);
401
- observed = scroller
402
- ? { contentEl, headerEl, scroller, scrollContent: scroller.firstElementChild }
403
- : null;
404
-
405
- resizeObserver?.disconnect();
406
- resizeObserver = new ResizeObserver(measure);
407
- if (contentEl?.isConnected) resizeObserver.observe(contentEl);
408
- if (headerEl) resizeObserver.observe(headerEl);
409
- if (scroller) {
410
- resizeObserver.observe(scroller);
411
- if (scroller.firstElementChild) resizeObserver.observe(scroller.firstElementChild);
412
- }
413
- measure();
414
- };
415
-
416
- observe();
417
- // Watch the whole drawer subtree — catches the scrollable mounting/
418
- // unmounting AND the layout-branch swap, which remounts the content node
419
- // (a content-scoped observer would go stale after the swap).
420
- mutationObserver = new MutationObserver(observe);
421
- mutationObserver.observe(drawerEl, { childList: true, subtree: true });
422
- };
423
-
424
- rafId = window.requestAnimationFrame(attach);
425
-
523
+ // Flush styles so this commit's snap transition shows up in getAnimations()
524
+ // eslint-disable-next-line no-void
525
+ void drawer.offsetHeight;
526
+ Promise.allSettled(drawer.getAnimations().map((a) => a.finished)).then(() => {
527
+ if (!canceled) revealFocusedElement(drawer);
528
+ });
426
529
  return () => {
427
530
  canceled = true;
428
- window.cancelAnimationFrame(rafId);
429
- mutationObserver?.disconnect();
430
- resizeObserver?.disconnect();
431
531
  };
432
- }, [isOpen, hasAutoDetent, measureNaturalHeight]);
532
+ }, [keyboardViewport]);
433
533
 
434
534
  // Below the last detent a vertical touch pan moves the sheet, not the
435
535
  // content — `[data-vaul-scroll-locked]` disables vertical touch panning on
@@ -437,13 +537,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
437
537
  const isScrollLocked =
438
538
  validDetents.length > 0 && activeSnapPoint !== validDetents[validDetents.length - 1];
439
539
 
440
- // Vaul measures the auto-size wrapper's offsetHeight (always, post fork).
441
- // Track it here so the form sheet can size its card to fit content,
442
- // clamped between a minimum ratio of the viewport and a maximum derived
443
- // from `detachedOffset` (the breathing room left at top + bottom of the
444
- // floating card).
445
- const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
446
-
447
540
  const effectiveMaxContentHeight = useMemo<number | undefined>(() => {
448
541
  if (maxContentHeight !== undefined) return maxContentHeight;
449
542
  if (!isFormSheet) return undefined;
@@ -510,7 +603,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
510
603
  effectiveMaxContentHeight,
511
604
  peekHeight,
512
605
  peekContentHeight,
513
- measuredContentHeight,
606
+ keyboardViewport,
514
607
  });
515
608
  geometryInputsRef.current = {
516
609
  effectiveDetached,
@@ -518,26 +611,26 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
518
611
  effectiveMaxContentHeight,
519
612
  peekHeight,
520
613
  peekContentHeight,
521
- measuredContentHeight,
614
+ keyboardViewport,
522
615
  };
523
616
 
524
617
  const computeDetentGeometry = useCallback(() => {
525
618
  const inputs = geometryInputsRef.current;
526
- const windowH = window.innerHeight;
527
- const effectiveH = inputs.effectiveDetached
528
- ? windowH - inputs.effectiveDetachedOffset
529
- : windowH;
619
+ const windowH = inputs.keyboardViewport?.height ?? window.innerHeight;
620
+ const bottomGap = inputs.effectiveDetached ? inputs.effectiveDetachedOffset : 0;
621
+ const effectiveH = windowH - bottomGap;
622
+ // Above a keyboard, fractions keep their full-height size (see vaul).
623
+ const fractionH = inputs.keyboardViewport
624
+ ? inputs.keyboardViewport.layoutHeight - bottomGap
625
+ : effectiveH;
530
626
  // Matches vaul's height ceiling: min(effectiveH, maxContentHeight).
531
627
  const ceiling =
532
628
  inputs.effectiveMaxContentHeight !== undefined
533
629
  ? Math.min(effectiveH, inputs.effectiveMaxContentHeight)
534
630
  : effectiveH;
535
- // 'auto' resolves to the content's natural height. Read it live — the
536
- // `measuredContentHeight` state lags mount by a frame (vaul's initial
537
- // ref-callback measure runs while the portal subtree is still detached,
538
- // so it reads 0 until the ResizeObserver fires). Falls back to the state
539
- // value, then to vaul's pre-measure fallback (effectiveHeight / 2).
540
- const measuredHeight = measureNaturalHeight() || inputs.measuredContentHeight;
631
+ // Reuse the intrinsic measurement during position events so animation
632
+ // frames don't clone the content tree again.
633
+ const measuredHeight = naturalHeightRef.current;
541
634
  const autoHeight = Math.min(measuredHeight > 0 ? measuredHeight : effectiveH / 2, ceiling);
542
635
 
543
636
  // 'peek' is measured live from the DOM (offsetHeight is layout-based, so
@@ -565,7 +658,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
565
658
  for (const d of validDetentsRef.current) {
566
659
  const h =
567
660
  typeof d === 'number'
568
- ? Math.min(d * effectiveH, ceiling)
661
+ ? Math.min(d * fractionH, ceiling)
569
662
  : d === 'peek'
570
663
  ? Math.min(livePeekHeight, ceiling)
571
664
  : autoHeight;
@@ -573,7 +666,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
573
666
  values.push(effectiveH > 0 ? h / effectiveH : 0);
574
667
  }
575
668
  return { windowH, effectiveH, ceiling, positions, values };
576
- }, [measureNaturalHeight]);
669
+ }, []);
577
670
 
578
671
  // Detent info for lifecycle events. Position/detent come from the active
579
672
  // detent's target geometry — not the live DOM rect — so willPresent (drawer
@@ -652,54 +745,27 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
652
745
  [computeDetentGeometry]
653
746
  );
654
747
 
655
- // Mirror native synchronous layout: while dragging (and through the post-
656
- // release settle) the sized layout tracks the sheet's visible height in
657
- // realtime, clamped to the smallest detent — dragging below it slides the
658
- // sheet out without resizing. Once settled, the inline height is cleared so
659
- // the CSS calc() owns sizing again (adapts to viewport resizes at rest).
660
748
  const sizedLayoutRef = useRef<HTMLDivElement>(null);
661
749
  const isDraggingRef = useRef(false);
662
- const isSettlingAfterDragRef = useRef(false);
663
- const updateSizedLayoutHeight = useCallback(
664
- (position: number) => {
665
- const node = sizedLayoutRef.current;
666
- if (!node) return;
667
-
668
- const { effectiveH, ceiling, positions } = computeDetentGeometry();
669
- if (positions.length === 0) return;
670
-
671
- if (isSettlingAfterDragRef.current) {
672
- const snap = activeSnapPointRef.current;
673
- const index = snap != null ? validDetentsRef.current.indexOf(snap) : -1;
674
- const target = index >= 0 ? positions[index]! : null;
675
- if (target != null && Math.abs(position - target) < 1) {
676
- isSettlingAfterDragRef.current = false;
677
- // Restore the calc — height is an inline style, so clearing it would
678
- // leave the div unsized (React won't re-apply it without a render)
679
- node.style.height = SIZED_LAYOUT_HEIGHT;
680
- return;
681
- }
682
- }
683
-
684
- // positions[0] is the smallest detent's top-Y — its height is the clamp floor.
685
- const minHeight = effectiveH - positions[0]!;
686
- const height = Math.min(Math.max(effectiveH - position, minHeight), ceiling);
687
- node.style.height = `${height}px`;
688
- },
689
- [computeDetentGeometry]
690
- );
691
750
 
692
751
  const handlePositionChange = useCallback(
693
752
  (position: number) => {
694
- if (isDraggingRef.current || isSettlingAfterDragRef.current) {
695
- updateSizedLayoutHeight(position);
696
- }
697
753
  const { index, detent } = interpolateFromPosition(position);
754
+ if (isOpenRef.current && !isPresentingRef.current) {
755
+ const { effectiveH, positions } = computeDetentGeometry();
756
+ setDisplayedBackgroundIndex((current) =>
757
+ getDetentBackgroundIndex(
758
+ effectiveH - position,
759
+ positions.map((top) => effectiveH - top),
760
+ current
761
+ )
762
+ );
763
+ }
698
764
  onPositionChangeRef.current?.({
699
765
  nativeEvent: { index, position, detent, realtime: true },
700
766
  } as PositionChangeEvent);
701
767
  },
702
- [interpolateFromPosition, updateSizedLayoutHeight]
768
+ [interpolateFromPosition, computeDetentGeometry]
703
769
  );
704
770
 
705
771
  // Fire onMount once after first render. React-mount is the earliest point
@@ -726,6 +792,12 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
726
792
 
727
793
  const present = isOpen;
728
794
  if (!present) {
795
+ // Mirror native: dismissing a sheet takes every sheet stacked above it
796
+ // (iOS dismisses from the presenter; Android runs dismissStack first).
797
+ dismissDescendantsRef.current();
798
+ // Mirror native's lazy-mount cancel: dismissed before willPresent, no events
799
+ if (!didEmitWillPresentRef.current) return undefined;
800
+ didEmitWillPresentRef.current = false;
729
801
  // Pair willBlur with willDismiss on dismiss — mirrors native iOS
730
802
  // emitWillDismissEvents (blur fires before dismiss). willPresent is
731
803
  // deferred to `start()` below (needs the mounted drawer's geometry).
@@ -735,11 +807,21 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
735
807
 
736
808
  const fireDone = () => {
737
809
  if (present) {
810
+ isPresentingRef.current = false;
811
+ // A dismiss may already be underway (it lands on the next commit)
812
+ if (phaseRef.current === 'presenting') phaseRef.current = 'presented';
738
813
  onDidPresentRef.current?.({ nativeEvent: computeDetentInfo() } as DidPresentEvent);
739
814
  onDidFocusRef.current?.({ nativeEvent: null } as DidFocusEvent);
815
+ settle(resolvePresentRef);
816
+ if (shouldDismissOnPresentRef.current) {
817
+ shouldDismissOnPresentRef.current = false;
818
+ setIsOpen(false);
819
+ }
740
820
  } else {
821
+ phaseRef.current = 'dismissed';
741
822
  onDidBlurRef.current?.({ nativeEvent: null } as DidBlurEvent);
742
823
  onDidDismissRef.current?.({ nativeEvent: null } as DidDismissEvent);
824
+ settle(resolveDismissRef);
743
825
  }
744
826
  };
745
827
 
@@ -765,6 +847,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
765
847
  // willPresent mirrors native iOS where viewWillAppear dispatches
766
848
  // both; the descendant-stack focus effect handles subsequent
767
849
  // transitions.
850
+ didEmitWillPresentRef.current = true;
768
851
  onWillPresentRef.current?.({ nativeEvent: computeDetentInfo() } as WillPresentEvent);
769
852
  onWillFocusRef.current?.({ nativeEvent: null } as WillFocusEvent);
770
853
  }
@@ -824,29 +907,31 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
824
907
  }
825
908
  onDragChangeRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragChangeEvent);
826
909
  }, [computeDetentInfo]);
827
- const handleRelease = useCallback(
828
- (_event: unknown, open: boolean) => {
829
- if (!isDraggingRef.current) return;
830
- isDraggingRef.current = false;
831
- // Track the sized layout through the settle animation only when the sheet
832
- // stays open — a dismissal slides out at the min-clamped height.
833
- isSettlingAfterDragRef.current = open;
834
- onDragEndRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragEndEvent);
835
- },
836
- [computeDetentInfo]
837
- );
910
+ const handleRelease = useCallback(() => {
911
+ if (!isDraggingRef.current) return;
912
+ isDraggingRef.current = false;
913
+ onDragEndRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragEndEvent);
914
+ }, [computeDetentInfo]);
838
915
 
839
916
  const { isNested, dismissAbove, descendants } = useSheetStack(
840
- methodsRef,
917
+ handleRef,
841
918
  drawerContentRef,
842
919
  isOpen,
843
920
  isFormSheet
844
921
  );
845
922
  dismissAboveRef.current = dismissAbove;
923
+ // Captured per render: by the time the dismiss effect runs, this sheet has
924
+ // already been popped from the stack, so `dismissAbove` can't find it.
925
+ dismissDescendantsRef.current = () => {
926
+ for (let i = descendants.length - 1; i >= 0; i--) {
927
+ descendants[i]!.ref.current?.dismissQuietly();
928
+ }
929
+ };
846
930
 
847
931
  // Mirror Android: translate this sheet down to match the deepest descendant's
848
932
  // top so the whole stack visually aligns. Cascades because every ancestor
849
933
  // re-runs whenever the stack (and thus its descendants) changes.
934
+ const wasCascadedRef = useRef(false);
850
935
  useEffect(() => {
851
936
  const parent = drawerContentRef.current;
852
937
  if (!parent) return;
@@ -858,7 +943,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
858
943
  if (!isOpen) return;
859
944
  const parentWrapper = parent.closest<HTMLElement>('[data-vaul-detached-wrapper]');
860
945
 
861
- const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
946
+ const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')}), --snap-point-height ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
862
947
  const wrapperTransition = `clip-path ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
863
948
  const CLIP_NONE = 'inset(0px round 0px)';
864
949
 
@@ -882,23 +967,32 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
882
967
  };
883
968
 
884
969
  if (descendants.length === 0) {
970
+ if (!wasCascadedRef.current) return;
971
+ wasCascadedRef.current = false;
885
972
  parent.style.transition = transition;
886
973
  parent.style.transform = '';
887
974
  setClip(CLIP_NONE);
888
975
  return;
889
976
  }
890
-
891
- // Track only the immediate child's snap point. Walking deeper descendants
892
- // would push this sheet further when a grandchild opens, even when our
893
- // own child didn't move (e.g., child skipped its cascade for a page
894
- // grandchild) — leaving a visible gap between this sheet and its child.
977
+ wasCascadedRef.current = true;
978
+
979
+ // Track the immediate child's *actual* top: its cascade transform when it
980
+ // was itself pushed behind a grandchild, else its own snap point. Mirrors
981
+ // Android's addTranslation propagating extra translation up the stack —
982
+ // a grandchild opening pushes this sheet only as far as our child moved.
983
+ const readTranslateY = (el: HTMLElement) => {
984
+ const match = el.style.transform.match(/translate3d\([^,]*,\s*(-?\d*\.?\d+)px/);
985
+ return match ? Number.parseFloat(match[1]!) : null;
986
+ };
895
987
  const computeTargetY = () => {
896
988
  const parentSnap =
897
989
  Number.parseFloat(parent.style.getPropertyValue('--snap-point-height')) || 0;
898
990
  const node = descendants[0]?.nodeRef.current;
899
991
  if (!node) return parentSnap;
900
- const childSnap = Number.parseFloat(node.style.getPropertyValue('--snap-point-height')) || 0;
901
- return Math.max(parentSnap, childSnap);
992
+ const childTop =
993
+ readTranslateY(node) ??
994
+ (Number.parseFloat(node.style.getPropertyValue('--snap-point-height')) || 0);
995
+ return Math.max(parentSnap, childTop);
902
996
  };
903
997
 
904
998
  // When a form-sheet parent has a form-sheet descendant, clip the parent to
@@ -934,18 +1028,24 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
934
1028
 
935
1029
  const apply = () => {
936
1030
  applyFormClip();
937
- // Mirror iOS: a page-sheet child fully covers a form-sheet parent, so the
938
- // cascade push-down has no visible effect — and would briefly peek above
939
- // the page during the present animation. Leave the parent put.
940
- const child = descendants[0];
941
- if (isFormSheet && child && !child.isFormSheetRef.current) {
1031
+ // Mirror iPad/Android (`isExpanded`): a parent at full height (detent 1)
1032
+ // stays put and the child simply overlays it. Full height is the sheet's
1033
+ // own ceiling — the viewport for a page sheet, the capped content-fit
1034
+ // height for a form sheet — so compare against where detent 1 lands.
1035
+ const parentSnap =
1036
+ Number.parseFloat(parent.style.getPropertyValue('--snap-point-height')) || 0;
1037
+ const { effectiveH, ceiling } = computeDetentGeometry();
1038
+ if (parentSnap <= effectiveH - ceiling + 0.5) {
942
1039
  parent.style.transition = transition;
943
1040
  parent.style.transform = '';
944
1041
  return;
945
1042
  }
1043
+ // Mirror Android: stay put while the child is being dragged (its live
1044
+ // transform would drag us along during drag-to-dismiss). The release
1045
+ // snap rewrites the child's style and re-applies.
1046
+ if (descendants[0]?.nodeRef.current?.classList.contains(DRAG_CLASS)) return;
946
1047
  const targetY = computeTargetY();
947
- const match = parent.style.transform.match(/translate3d\([^,]*,\s*(-?\d*\.?\d+)px/);
948
- const currentY = match ? Number.parseFloat(match[1]!) : 0;
1048
+ const currentY = readTranslateY(parent) ?? 0;
949
1049
  if (Math.abs(currentY - targetY) < 0.5) return;
950
1050
  parent.style.transition = transition;
951
1051
  parent.style.transform = `translate3d(0, ${targetY}px, 0)`;
@@ -957,12 +1057,15 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
957
1057
  // inline style changes.
958
1058
  const observer = new MutationObserver(apply);
959
1059
  observer.observe(parent, { attributes: true, attributeFilter: ['style'] });
1060
+ // The child's own cascade lands in a later frame — follow it when it moves.
1061
+ const childNode = descendants[0]?.nodeRef.current;
1062
+ if (childNode) observer.observe(childNode, { attributes: true, attributeFilter: ['style'] });
960
1063
 
961
1064
  return () => {
962
1065
  cancelAnimationFrame(raf);
963
1066
  observer.disconnect();
964
1067
  };
965
- }, [descendants, activeSnapPoint, cornerRadius, isFormSheet, isOpen]);
1068
+ }, [descendants, activeSnapPoint, cornerRadius, isFormSheet, isOpen, computeDetentGeometry]);
966
1069
 
967
1070
  // Focus/blur events fire when a descendant sheet appears on top of this one
968
1071
  // (blur) or when all descendants are dismissed (focus). will-events fire
@@ -973,12 +1076,17 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
973
1076
  const onDidBlurRef = useRef(onDidBlur);
974
1077
  const onWillFocusRef = useRef(onWillFocus);
975
1078
  const onDidFocusRef = useRef(onDidFocus);
1079
+ const onDismissAttemptRef = useRef(onDismissAttempt);
976
1080
  useEffect(() => {
977
1081
  onWillBlurRef.current = onWillBlur;
978
1082
  onDidBlurRef.current = onDidBlur;
979
1083
  onWillFocusRef.current = onWillFocus;
980
1084
  onDidFocusRef.current = onDidFocus;
1085
+ onDismissAttemptRef.current = onDismissAttempt;
981
1086
  });
1087
+ const handleDismissAttempt = useCallback(() => {
1088
+ onDismissAttemptRef.current?.({ nativeEvent: null } as DismissAttemptEvent);
1089
+ }, []);
982
1090
 
983
1091
  const prevDescendantCountRef = useRef(0);
984
1092
  useEffect(() => {
@@ -1026,11 +1134,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
1026
1134
  };
1027
1135
  }, [isOpen, descendants.length]);
1028
1136
 
1029
- // Definite-height flex layout (per-detent sizing) unless content must be
1030
- // measured in natural flow: 'auto' detents (without a plugged scrollable —
1031
- // those measure via `measureNaturalHeight`) and form-sheet content-fit sizing.
1032
- const useSizedLayout = (!hasAutoDetent || hasBoundedScrollable) && !isFormSheet;
1033
-
1034
1137
  const effectiveCornerRadius = cornerRadius ?? DEFAULT_CORNER_RADIUS;
1035
1138
 
1036
1139
  // Shadow cast upward from the sheet's top edge toward the background. Matches
@@ -1048,6 +1151,24 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
1048
1151
  ? `drop-shadow(0 ${-elevation}px ${elevation * 3}px rgba(0, 0, 0, 0.15))`
1049
1152
  : undefined;
1050
1153
 
1154
+ const backgroundLayerStyle: React.CSSProperties = {
1155
+ position: 'absolute',
1156
+ inset: 0,
1157
+ pointerEvents: 'none',
1158
+ backgroundColor: backgroundColor as string,
1159
+ transition: isPresentingRef.current ? 'none' : 'background-color 200ms',
1160
+ };
1161
+
1162
+ // Vaul fades the overlay through its opacity, so the dim opacity goes into the color
1163
+ const resolvedOverlayStyle = useMemo<React.CSSProperties>(() => {
1164
+ if (dimColor == null && dimOpacity == null) return overlayStyle;
1165
+ const percentage = Math.min(1, Math.max(0, dimOpacity ?? DEFAULT_DIM_OPACITY)) * 100;
1166
+ return {
1167
+ ...overlayStyle,
1168
+ backgroundColor: `color-mix(in srgb, ${String(dimColor ?? 'black')} ${percentage}%, transparent)`,
1169
+ };
1170
+ }, [dimColor, dimOpacity]);
1171
+
1051
1172
  const mergedContentStyle = useMemo<React.CSSProperties>(
1052
1173
  () => ({
1053
1174
  position: 'fixed',
@@ -1059,7 +1180,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
1059
1180
  flexDirection: 'column',
1060
1181
  borderTopLeftRadius: effectiveCornerRadius,
1061
1182
  borderTopRightRadius: effectiveCornerRadius,
1062
- backgroundColor: backgroundColor as string,
1063
1183
  // Clip children to the rounded top so headers/content with their own
1064
1184
  // background don't bleed past the corners. `clip` (not `hidden`): a
1065
1185
  // hidden box is still a scroll container, so browser focus-reveal (e.g.
@@ -1069,12 +1189,24 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
1069
1189
  // Lift content above iOS home indicator / bottom safe area when enabled.
1070
1190
  // A relative footer owns the inset instead (see resolvedFooterStyle); an
1071
1191
  // absolute footer floats over the content, so the content keeps its lift.
1192
+ // A plugged scrollable keeps the lift too (contentInsetAdjustment
1193
+ // 'automatic' | 'safe-area') — 'footer' | 'never' scroll edge-to-edge
1194
+ // behind the indicator with user-applied content padding.
1072
1195
  paddingBottom:
1073
- insetAdjustment === 'automatic' && !(footerOwnsInset && !absoluteFooter)
1196
+ insetAdjustment === 'automatic' &&
1197
+ !(footerOwnsInset && !absoluteFooter) &&
1198
+ (!scrollableRef || insetBehavior === 'automatic' || insetBehavior === 'safe-area')
1074
1199
  ? 'env(safe-area-inset-bottom, 0px)'
1075
1200
  : 0,
1076
1201
  }),
1077
- [backgroundColor, effectiveCornerRadius, insetAdjustment, footerOwnsInset, absoluteFooter]
1202
+ [
1203
+ effectiveCornerRadius,
1204
+ insetAdjustment,
1205
+ footerOwnsInset,
1206
+ absoluteFooter,
1207
+ scrollableRef,
1208
+ insetBehavior,
1209
+ ]
1078
1210
  );
1079
1211
 
1080
1212
  const defaultGrabberColor =
@@ -1112,7 +1244,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
1112
1244
  // `maxContentWidth` is ignored ('form' is absolute).
1113
1245
  // - presentation='page' → `maxContentWidth` (any viewport) or
1114
1246
  // DEFAULT_MAX_WIDTH (tablet/landscape readability cap).
1115
- // Detached without a width constraint applies anchorOffset on both edges so
1247
+ // Detached without a width constraint applies placementOffset on both edges so
1116
1248
  // the floating card breathes from the viewport sides.
1117
1249
  const wrapperStyle = useMemo<React.CSSProperties | undefined>(() => {
1118
1250
  // Mobile portrait ignores width sizing entirely (matches iOS/Android:
@@ -1131,30 +1263,30 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
1131
1263
  if (dropShadow) next.filter = dropShadow;
1132
1264
  if (!needsMargins) return next;
1133
1265
 
1134
- let marginLeft: number | string;
1135
- let marginRight: number | string;
1266
+ let marginInlineStart: number | string;
1267
+ let marginInlineEnd: number | string;
1136
1268
  if (isFormSheet) {
1137
- marginLeft = 'auto';
1138
- marginRight = 'auto';
1269
+ marginInlineStart = 'auto';
1270
+ marginInlineEnd = 'auto';
1139
1271
  } else if (maxWidth == null) {
1140
- marginLeft = anchorOffset;
1141
- marginRight = anchorOffset;
1272
+ marginInlineStart = placementOffset;
1273
+ marginInlineEnd = placementOffset;
1142
1274
  } else {
1143
- marginLeft = anchor === 'left' ? anchorOffset : 'auto';
1144
- marginRight = anchor === 'right' ? anchorOffset : 'auto';
1275
+ marginInlineStart = placement === 'leading' ? placementOffset : 'auto';
1276
+ marginInlineEnd = placement === 'trailing' ? placementOffset : 'auto';
1145
1277
  }
1146
1278
 
1147
1279
  if (maxWidth != null) next.maxWidth = maxWidth;
1148
- next.marginLeft = marginLeft;
1149
- next.marginRight = marginRight;
1280
+ next.marginInlineStart = marginInlineStart;
1281
+ next.marginInlineEnd = marginInlineEnd;
1150
1282
  return next;
1151
1283
  }, [
1152
1284
  isLandscapeOrTablet,
1153
1285
  isFormSheet,
1154
1286
  maxContentWidth,
1155
1287
  presentation,
1156
- anchor,
1157
- anchorOffset,
1288
+ placement,
1289
+ placementOffset,
1158
1290
  effectiveDetached,
1159
1291
  dropShadow,
1160
1292
  ]);
@@ -1191,6 +1323,8 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
1191
1323
  onDrag={handleDrag}
1192
1324
  onRelease={handleRelease}
1193
1325
  dismissible={dismissible}
1326
+ dismissThreshold={dismissThreshold}
1327
+ onDismissAttempt={handleDismissAttempt}
1194
1328
  draggable={draggable}
1195
1329
  repositionInputs={false}
1196
1330
  modal={dimmed}
@@ -1200,24 +1334,18 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
1200
1334
  detachedRadius={effectiveCornerRadius}
1201
1335
  maxContentHeight={effectiveMaxContentHeight}
1202
1336
  peekHeight={peekHeight}
1203
- initialAnimated={initialDetentAnimated}
1337
+ initialAnimated={shouldAutoPresent ? initialDetentAnimated : true}
1204
1338
  detachedWrapperStyle={wrapperStyle}
1205
- onContentHeightChange={setMeasuredContentHeight}
1206
- contentHeight={
1207
- // Bounded scrollable → the auto-size wrapper measures 0 (sized
1208
- // layout is absolute), so feed vaul the natural height instead.
1209
- hasAutoDetent && hasBoundedScrollable && scrollableAutoHeight > 0
1210
- ? scrollableAutoHeight
1211
- : undefined
1212
- }
1339
+ contentHeight={measuredContentHeight}
1340
+ keyboardViewport={keyboardViewport}
1213
1341
  activeSnapPoint={activeSnapPoint}
1214
1342
  setActiveSnapPoint={handleSetActiveSnapPoint}
1215
1343
  {...snapPointsProps}
1216
1344
  >
1217
1345
  <Drawer.Portal container={portalContainer ?? undefined}>
1218
- <Drawer.Overlay style={overlayStyle} />
1346
+ <Drawer.Overlay style={resolvedOverlayStyle} />
1219
1347
  <Drawer.Content
1220
- ref={drawerContentRef}
1348
+ ref={handleDrawerRef}
1221
1349
  style={mergedContentStyle}
1222
1350
  onPointerDownOutside={handlePointerDownOutside}
1223
1351
  detachedSiblings={
@@ -1230,66 +1358,35 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
1230
1358
  ) : undefined
1231
1359
  }
1232
1360
  >
1361
+ <div aria-hidden style={backgroundLayerStyle} />
1233
1362
  <Drawer.Title style={visuallyHiddenStyle}>
1234
1363
  {accessibilityOptions?.paneTitle ?? 'Sheet'}
1235
1364
  </Drawer.Title>
1236
1365
  {grabber && <Drawer.Handle style={handleStyle} />}
1237
- {useSizedLayout ? (
1238
- // vaul wraps children in `[data-vaul-auto-size-wrapper]` (display:
1239
- // flow-root) which doesn't honor descendant flex layout. Use an
1240
- // absolute fill sized to the visible portion (via vaul's
1241
- // `--snap-point-height` var) so the inner flex column has a
1242
- // definite height for flex layouts to fill — mirrors native, where
1243
- // the container is sized to the sheet's visible height per detent.
1244
- <div
1245
- ref={sizedLayoutRef}
1246
- style={sizedLayoutStyle}
1247
- data-vaul-scroll-locked={isScrollLocked ? '' : undefined}
1248
- >
1249
- {header && (
1250
- <View ref={headerElRef} style={resolvedHeaderStyle} onLayout={handleHeaderLayout}>
1251
- {isValidElement(header) ? header : createElement(header)}
1252
- </View>
1253
- )}
1254
- {/* Content lays out naturally like native — fill only for auto
1255
- detents with a plugged scrollable, where natural layout is
1256
- circular (sheet height derives from the scroll content size).
1257
- Mirrors the native shadow node's scrollableBounded behavior. */}
1366
+ <div
1367
+ ref={sizedLayoutRef}
1368
+ style={sizedLayoutStyle}
1369
+ data-vaul-scroll-locked={isScrollLocked ? '' : undefined}
1370
+ >
1371
+ <TrueSheetBackground background={background} backgroundStyle={backgroundStyle} />
1372
+ {header && (
1373
+ <View ref={headerElRef} style={resolvedHeaderStyle} onLayout={handleHeaderLayout}>
1374
+ {isValidElement(header) ? header : createElement(header)}
1375
+ </View>
1376
+ )}
1377
+ <View ref={contentRef} style={[contentStyle, style]} onLayout={handleContentLayout}>
1378
+ {children}
1379
+ </View>
1380
+ {footer && !absoluteFooter && (
1258
1381
  <View
1259
- ref={contentRef}
1260
- style={hasAutoDetent && hasBoundedScrollable ? [contentFillStyle, style] : style}
1382
+ ref={footerElRef}
1383
+ style={[relativeFooterStyle, resolvedFooterStyle]}
1384
+ onLayout={handleFooterLayout}
1261
1385
  >
1262
- {children}
1386
+ {isValidElement(footer) ? footer : createElement(footer)}
1263
1387
  </View>
1264
- {footer && !absoluteFooter && (
1265
- <View
1266
- ref={footerElRef}
1267
- style={[relativeFooterStyle, resolvedFooterStyle]}
1268
- onLayout={handleFooterLayout}
1269
- >
1270
- {isValidElement(footer) ? footer : createElement(footer)}
1271
- </View>
1272
- )}
1273
- </div>
1274
- ) : (
1275
- // Natural flow so vaul can measure content height — required for
1276
- // 'auto' detents and form-sheet content-fit sizing.
1277
- <>
1278
- {header && (
1279
- <View ref={headerElRef} style={resolvedHeaderStyle} onLayout={handleHeaderLayout}>
1280
- {isValidElement(header) ? header : createElement(header)}
1281
- </View>
1282
- )}
1283
- <View ref={contentRef} style={style}>
1284
- {children}
1285
- </View>
1286
- {footer && !absoluteFooter && (
1287
- <View ref={footerElRef} style={resolvedFooterStyle} onLayout={handleFooterLayout}>
1288
- {isValidElement(footer) ? footer : createElement(footer)}
1289
- </View>
1290
- )}
1291
- </>
1292
- )}
1388
+ )}
1389
+ </div>
1293
1390
  </Drawer.Content>
1294
1391
  </Drawer.Portal>
1295
1392
  </Drawer.Root>
@@ -1297,6 +1394,8 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
1297
1394
  );
1298
1395
  });
1299
1396
 
1397
+ const DEFAULT_DIM_OPACITY = 0.5;
1398
+
1300
1399
  const overlayStyle: React.CSSProperties = {
1301
1400
  position: 'fixed',
1302
1401
  inset: 0,
@@ -1327,12 +1426,11 @@ const sizedLayoutStyle: React.CSSProperties = {
1327
1426
  height: SIZED_LAYOUT_HEIGHT,
1328
1427
  display: 'flex',
1329
1428
  flexDirection: 'column',
1429
+ boxSizing: 'border-box',
1430
+ paddingBottom: 'inherit',
1330
1431
  };
1331
1432
 
1332
- const contentFillStyle = {
1333
- flex: 1,
1334
- minHeight: 0,
1335
- } as const;
1433
+ const contentStyle = { flexShrink: 1, minHeight: 0 } as const;
1336
1434
 
1337
1435
  const visuallyHiddenStyle: React.CSSProperties = {
1338
1436
  position: 'absolute',
@@ -1346,6 +1444,20 @@ const visuallyHiddenStyle: React.CSSProperties = {
1346
1444
  border: 0,
1347
1445
  };
1348
1446
 
1447
+ type SheetPhase = 'dismissed' | 'presenting' | 'presented' | 'dismissing';
1448
+
1449
+ const settle = (resolveRef: RefObject<(() => void) | null>) => {
1450
+ const resolve = resolveRef.current;
1451
+ resolveRef.current = null;
1452
+ resolve?.();
1453
+ };
1454
+
1455
+ // Same as native
1456
+ const ALREADY_PRESENTED_WARNING =
1457
+ 'TrueSheet: sheet is already presented. Use resize() to change detent.';
1458
+ const ALREADY_DISMISSED_WARNING =
1459
+ 'TrueSheet: sheet is already dismissed. No need to dismiss it again.';
1460
+
1349
1461
  const STATIC_METHOD_ERROR =
1350
1462
  'Static methods are not supported on web. Use the useTrueSheet() hook instead.';
1351
1463