@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
@@ -1,27 +1,26 @@
1
1
  "use strict";
2
2
 
3
3
  /// <reference lib="dom" />
4
- import { createElement, forwardRef, isValidElement, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
4
+ import { createElement, forwardRef, isValidElement, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react';
5
5
  import { StyleSheet, useColorScheme, useWindowDimensions, View } from 'react-native';
6
6
  import { measurePeekContentHeight, TrueSheetPeekContext } from "./TrueSheetPeek.web.js";
7
7
  import { usePortalContainer, useRegisterSheet, useSheetStack } from "./TrueSheetProvider.web.js";
8
- import { COLOR_SURFACE_CONTAINER_LOW_DARK, COLOR_SURFACE_CONTAINER_LOW_LIGHT, DEFAULT_ANCHOR_OFFSET, DEFAULT_CORNER_RADIUS, DEFAULT_DETACHED_OFFSET, DEFAULT_FORM_SHEET_HEIGHT_RATIO, DEFAULT_FORM_SHEET_WIDTH, DEFAULT_GRABBER_COLOR_DARK, DEFAULT_GRABBER_COLOR_LIGHT, DEFAULT_GRABBER_HEIGHT, DEFAULT_GRABBER_TOP_MARGIN, DEFAULT_GRABBER_WIDTH, DEFAULT_MAX_WIDTH } from "./web/constants.js";
8
+ import { COLOR_SURFACE_CONTAINER_LOW_DARK, COLOR_SURFACE_CONTAINER_LOW_LIGHT, DEFAULT_PLACEMENT_OFFSET, DEFAULT_CORNER_RADIUS, DEFAULT_DETACHED_OFFSET, DEFAULT_FORM_SHEET_HEIGHT_RATIO, DEFAULT_FORM_SHEET_WIDTH, DEFAULT_GRABBER_COLOR_DARK, DEFAULT_GRABBER_COLOR_LIGHT, DEFAULT_GRABBER_HEIGHT, DEFAULT_GRABBER_TOP_MARGIN, DEFAULT_GRABBER_WIDTH, DEFAULT_MAX_WIDTH } from "./web/constants.js";
9
9
  import { getDOMElement } from "./web/dom.js";
10
+ import { TrueSheetBackground } from "./TrueSheetBackground.js";
11
+ import { getDetentBackgroundIndex, normalizeDetentBackground } from "./detentBackgrounds.js";
12
+ import { revealFocusedElement, useKeyboardViewport } from "./web/keyboard.js";
13
+ import { observeSheetLayout } from "./web/layout.js";
14
+ import { resolveScrollable } from "./scrollable.js";
10
15
  import { Drawer } from "./web/vaul/index.js";
11
- import { DEFAULT_PEEK_HEIGHT, TRANSITIONS } from "./web/vaul/constants.js";
16
+ import { DEFAULT_PEEK_HEIGHT, DRAG_CLASS, TRANSITIONS } from "./web/vaul/constants.js";
12
17
 
13
- // First vertically scrollable descendant — web mirror of native's
14
- // findScrollView (RN-web ScrollView/FlatList render a node with
15
- // `overflow-y: auto`; its first child is the content container).
16
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
17
- const findVerticalScroller = root => {
18
- for (const el of root.querySelectorAll('*')) {
19
- const {
20
- overflowY
21
- } = window.getComputedStyle(el);
22
- if (overflowY === 'auto' || overflowY === 'scroll') return el;
23
- }
24
- return null;
18
+ // Resolves the user's `scrollableRef` to its scrollable DOM element. The
19
+ // node's first child is the content container.
20
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
21
+ const getScrollableElement = scrollable => {
22
+ const node = resolveScrollable(scrollable);
23
+ return node instanceof HTMLElement ? node : null;
25
24
  };
26
25
  const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
27
26
  const {
@@ -29,18 +28,26 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
29
28
  name,
30
29
  dismissible = true,
31
30
  draggable = true,
31
+ dismissThreshold = 'half',
32
32
  cornerRadius,
33
33
  style,
34
34
  backgroundColor: backgroundColorProp,
35
+ detentBackgrounds,
36
+ background,
37
+ backgroundStyle,
35
38
  maxContentHeight,
36
39
  maxContentWidth,
37
- anchor = 'center',
38
- anchorOffset = DEFAULT_ANCHOR_OFFSET,
40
+ placement = 'automatic',
41
+ placementOffset = DEFAULT_PLACEMENT_OFFSET,
42
+ scrollableRef,
43
+ scrollableOptions,
39
44
  grabber = true,
40
45
  grabberOptions,
41
46
  accessibilityOptions,
42
47
  detents = [0.5, 1],
43
48
  dimmed = true,
49
+ dimColor,
50
+ dimOpacity,
44
51
  dimmedDetentIndex = 0,
45
52
  initialDetentIndex = -1,
46
53
  header,
@@ -60,6 +67,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
60
67
  onDidPresent,
61
68
  onWillDismiss,
62
69
  onDidDismiss,
70
+ onDismissAttempt,
63
71
  onDetentChange,
64
72
  onDragBegin,
65
73
  onDragChange,
@@ -91,9 +99,26 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
91
99
  // is explicitly set.
92
100
  const effectiveDetached = isFormSheet || detached;
93
101
  const colorScheme = useColorScheme();
94
- const backgroundColor = backgroundColorProp ?? (colorScheme === 'dark' ? COLOR_SURFACE_CONTAINER_LOW_DARK : COLOR_SURFACE_CONTAINER_LOW_LIGHT);
95
102
  const shouldAutoPresent = initialDetentIndex >= 0 && initialDetentIndex < validDetents.length;
103
+ const didInitiallyPresentRef = useRef(shouldAutoPresent);
96
104
  const [isOpen, setIsOpen] = useState(shouldAutoPresent);
105
+ const isOpenRef = useRef(isOpen);
106
+ isOpenRef.current = isOpen;
107
+ const keyboardViewport = useKeyboardViewport(isOpen);
108
+ const isPresentingRef = useRef(shouldAutoPresent);
109
+ // Native's lifecycle state — updated synchronously so calls in the same tick see each other
110
+ const phaseRef = useRef(shouldAutoPresent ? 'presenting' : 'dismissed');
111
+ const resolvePresentRef = useRef(null);
112
+ const resolveDismissRef = useRef(null);
113
+ // Whether willPresent fired for the current open cycle — a present undone
114
+ // before it fires has nothing to pair a dismiss with.
115
+ const didEmitWillPresentRef = useRef(false);
116
+ // Whether the current open cycle came from initialDetentIndex. Native mounts those
117
+ // eagerly, so a dismiss before willPresent lands after the present instead of cancelling it.
118
+ const isAutoPresentRef = useRef(shouldAutoPresent);
119
+ const shouldDismissOnPresentRef = useRef(false);
120
+ const [displayedBackgroundIndex, setDisplayedBackgroundIndex] = useState(shouldAutoPresent ? initialDetentIndex : 0);
121
+ const backgroundColor = normalizeDetentBackground(detentBackgrounds?.[Math.min(displayedBackgroundIndex, validDetents.length - 1)])?.color ?? backgroundColorProp ?? (colorScheme === 'dark' ? COLOR_SURFACE_CONTAINER_LOW_DARK : COLOR_SURFACE_CONTAINER_LOW_LIGHT);
97
122
  const [activeSnapPoint, setActiveSnapPoint] = useState(() => validDetents[shouldAutoPresent ? initialDetentIndex : 0] ?? null);
98
123
 
99
124
  // Keep activeSnapPoint valid if detents change (e.g., prop updates).
@@ -103,15 +128,32 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
103
128
  }, [validDetents]);
104
129
  const validDetentsRef = useRef(validDetents);
105
130
  validDetentsRef.current = validDetents;
106
- const hasAutoDetent = validDetents.includes('auto');
107
131
  const handleSetActiveSnapPoint = useCallback(snapPoint => {
108
132
  setActiveSnapPoint(snapPoint == null ? null : typeof snapPoint === 'number' || snapPoint === 'auto' || snapPoint === 'peek' ? snapPoint : null);
109
133
  }, []);
110
- const handleOpenChange = useCallback(open => {
111
- if (!open && isOpen) {
134
+
135
+ // Returns null when there's nothing to dismiss. Resolves on didDismiss.
136
+ const dismissSheet = useCallback(() => {
137
+ if (phaseRef.current === 'dismissed' || phaseRef.current === 'dismissing') return null;
138
+ settle(resolvePresentRef);
139
+ if (didEmitWillPresentRef.current) {
140
+ setIsOpen(false);
141
+ } else if (isAutoPresentRef.current) {
142
+ shouldDismissOnPresentRef.current = true;
143
+ } else {
144
+ // Mirror native's lazy-mount cancel: nothing shown yet, nothing to animate out
112
145
  setIsOpen(false);
146
+ phaseRef.current = 'dismissed';
147
+ return Promise.resolve();
113
148
  }
114
- }, [isOpen]);
149
+ phaseRef.current = 'dismissing';
150
+ return new Promise(resolve => {
151
+ resolveDismissRef.current = resolve;
152
+ });
153
+ }, []);
154
+ const handleOpenChange = useCallback(open => {
155
+ if (!open) dismissSheet();
156
+ }, [dismissSheet]);
115
157
  const portalContainer = usePortalContainer();
116
158
  const handlePointerDownOutside = e => {
117
159
  const target = e.target;
@@ -122,29 +164,61 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
122
164
  e.preventDefault();
123
165
  return;
124
166
  }
167
+ if (!(target instanceof Element)) return;
168
+ const wrapper = drawerContentRef.current?.closest('[data-vaul-detached-wrapper]');
169
+ if (!wrapper) return;
125
170
  // The footer is rendered via vaul's `detachedSiblings` as a sibling of
126
171
  // Drawer.Content inside [data-vaul-detached-wrapper], so Radix treats
127
172
  // clicks on it as "outside" the content. Don't dismiss for clicks that
128
173
  // landed inside the wrapper.
129
- if (target instanceof Element) {
130
- const wrapper = drawerContentRef.current?.closest('[data-vaul-detached-wrapper]');
131
- if (wrapper && wrapper.contains(target)) {
132
- e.preventDefault();
133
- }
174
+ if (wrapper.contains(target)) {
175
+ e.preventDefault();
176
+ return;
134
177
  }
178
+ // Every sheet runs Radix non-modal and portals its overlay + wrapper as
179
+ // siblings into the shared container, so a press on a sheet stacked above
180
+ // this one (its content or its dim overlay) is also delivered here as
181
+ // "outside". Sheets present in document order, so anything after our
182
+ // wrapper belongs to a descendant. Radix may dispatch on the deferred
183
+ // click (touch), after a dismissing descendant has left the DOM — treat
184
+ // disconnected as above too.
185
+ /* eslint-disable no-bitwise */
186
+ const above = Node.DOCUMENT_POSITION_FOLLOWING | Node.DOCUMENT_POSITION_DISCONNECTED;
187
+ if (wrapper.compareDocumentPosition(target) & above) {
188
+ e.preventDefault();
189
+ }
190
+ /* eslint-enable no-bitwise */
135
191
  };
136
192
  const dismissAboveRef = useRef(async () => {});
193
+ const dismissDescendantsRef = useRef(() => {});
194
+ const presentSheet = useCallback(async (index, auto) => {
195
+ const detent = validDetentsRef.current[index];
196
+ if (detent === undefined) {
197
+ throw new Error(`TrueSheet: present index (${index}) is out of bounds. detents array has ${validDetentsRef.current.length} item(s)`);
198
+ }
199
+ if (phaseRef.current !== 'dismissed') {
200
+ console.warn(ALREADY_PRESENTED_WARNING);
201
+ return;
202
+ }
203
+ phaseRef.current = 'presenting';
204
+ isAutoPresentRef.current = auto;
205
+ isPresentingRef.current = true;
206
+ setDisplayedBackgroundIndex(index);
207
+ setActiveSnapPoint(detent);
208
+ setIsOpen(true);
209
+ await new Promise(resolve => {
210
+ resolvePresentRef.current = resolve;
211
+ });
212
+ }, []);
137
213
  const methods = useMemo(() => ({
138
- present: async (index = 0) => {
139
- const detent = validDetentsRef.current[index];
140
- if (detent === undefined) {
141
- throw new Error(`TrueSheet: present index (${index}) is out of bounds. detents array has ${validDetentsRef.current.length} item(s)`);
142
- }
143
- setActiveSnapPoint(detent);
144
- setIsOpen(true);
145
- },
214
+ present: (index = 0) => presentSheet(index, false),
146
215
  dismiss: async () => {
147
- setIsOpen(false);
216
+ const dismissal = dismissSheet();
217
+ if (!dismissal) {
218
+ console.warn(ALREADY_DISMISSED_WARNING);
219
+ return;
220
+ }
221
+ await dismissal;
148
222
  },
149
223
  resize: async index => {
150
224
  const detent = validDetentsRef.current[index];
@@ -154,13 +228,40 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
154
228
  setActiveSnapPoint(detent);
155
229
  },
156
230
  dismissStack: async animated => {
231
+ if (phaseRef.current === 'dismissed' || phaseRef.current === 'dismissing') {
232
+ console.warn(ALREADY_DISMISSED_WARNING);
233
+ return;
234
+ }
157
235
  await dismissAboveRef.current(animated);
158
236
  }
159
- }), []);
237
+ }), [presentSheet, dismissSheet]);
160
238
  useImperativeHandle(ref, () => methods, [methods]);
161
- const methodsRef = useRef(methods);
162
- useRegisterSheet(name, methodsRef);
239
+ useEffect(() => {
240
+ if (!shouldAutoPresent || didInitiallyPresentRef.current) return;
241
+ didInitiallyPresentRef.current = true;
242
+ if (phaseRef.current === 'dismissed') {
243
+ presentSheet(initialDetentIndex, true);
244
+ }
245
+ }, [initialDetentIndex, shouldAutoPresent, presentSheet]);
246
+
247
+ // Unmounted mid-transition: don't leave callers awaiting forever
248
+ useEffect(() => () => {
249
+ settle(resolvePresentRef);
250
+ settle(resolveDismissRef);
251
+ }, []);
252
+ const handleRef = useRef({
253
+ methods,
254
+ dismissQuietly: async () => {
255
+ await dismissSheet();
256
+ }
257
+ });
258
+ useRegisterSheet(name, handleRef);
163
259
  const drawerContentRef = useRef(null);
260
+ const [drawerContent, setDrawerContent] = useState(null);
261
+ const handleDrawerRef = useCallback(node => {
262
+ drawerContentRef.current = node;
263
+ setDrawerContent(node);
264
+ }, []);
164
265
 
165
266
  // Measured header/footer heights drive the 'peek' snap point — mirrors
166
267
  // native, where the controller tracks headerHeight/footerHeight.
@@ -185,17 +286,40 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
185
286
  const [peekContentHeight, setPeekContentHeight] = useState(0);
186
287
  const contentRef = useRef(null);
187
288
  const peekElRef = useRef(null);
289
+ const measurePeek = useCallback(() => {
290
+ const peekEl = getDOMElement(peekElRef.current);
291
+ const contentEl = getDOMElement(contentRef.current);
292
+ if (peekEl && contentEl) {
293
+ setPeekContentHeight(measurePeekContentHeight(peekEl, contentEl));
294
+ }
295
+ }, []);
296
+
297
+ // Last-attached wins — during a screen swap the incoming peek mounts before
298
+ // the outgoing one unmounts, so the outgoing detach must not clear it.
188
299
  const peekContext = useMemo(() => ({
189
300
  contentRef,
190
- peekRef: peekElRef,
191
- setPeekContentHeight
192
- }), []);
301
+ attachPeek: view => {
302
+ peekElRef.current = view;
303
+ measurePeek();
304
+ },
305
+ detachPeek: view => {
306
+ if (peekElRef.current !== view) return;
307
+ peekElRef.current = null;
308
+ setPeekContentHeight(0);
309
+ },
310
+ measurePeek: view => {
311
+ if (peekElRef.current === view) measurePeek();
312
+ }
313
+ }), [measurePeek]);
314
+
315
+ // The peek's own onLayout only fires when *it* resizes. Content added above
316
+ // it moves it without resizing it, so re-measure its offset whenever the
317
+ // content view's layout changes.
318
+ const handleContentLayout = measurePeek;
193
319
 
194
320
  // An absolute (floating) header overlaps the content, so it contributes no
195
321
  // height to the 'auto' detent measurement.
196
322
  const absoluteHeader = headerOptions?.position === 'absolute';
197
- const absoluteHeaderRef = useRef(absoluteHeader);
198
- absoluteHeaderRef.current = absoluteHeader;
199
323
  const resolvedHeaderStyle = absoluteHeader ? [absoluteHeaderStyle, headerStyle] : headerStyle;
200
324
 
201
325
  // Same for an absolute (floating) footer — rendered via vaul's
@@ -222,115 +346,56 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
222
346
  // A relative footer is laid out below the content, so it's pushed off-screen
223
347
  // at the peek detent and contributes no height.
224
348
  const peekHeight = (header ? headerHeight : 0) + (footer && absoluteFooter ? footerHeight : 0) + peekContentHeight || DEFAULT_PEEK_HEIGHT;
349
+ const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
350
+ const naturalHeightRef = useRef(0);
351
+ const insetBehavior = scrollableOptions?.contentInsetAdjustment ?? 'automatic';
352
+ const footerInsetAdjustment = Boolean(footer) && absoluteFooter && (insetBehavior === 'automatic' || insetBehavior === 'footer');
353
+ const measureContent = validDetents.includes('auto') || isFormSheet;
354
+ useLayoutEffect(() => {
355
+ const layoutEl = sizedLayoutRef.current;
356
+ if (!isOpen || !drawerContent || !layoutEl?.isConnected) return undefined;
357
+ setHeaderHeight(getDOMElement(headerElRef.current)?.offsetHeight ?? 0);
358
+ setFooterHeight(getDOMElement(footerElRef.current)?.offsetHeight ?? 0);
359
+ measurePeek();
360
+ const observer = observeSheetLayout({
361
+ drawer: drawerContent,
362
+ layout: layoutEl,
363
+ getContent: () => getDOMElement(contentRef.current),
364
+ getHeader: () => getDOMElement(headerElRef.current),
365
+ getFooter: () => getDOMElement(footerElRef.current),
366
+ getScrollable: () => getScrollableElement(scrollableRef?.current),
367
+ absoluteHeader,
368
+ absoluteFooter,
369
+ footerInsetAdjustment,
370
+ measureContent,
371
+ onHeightChange: height => {
372
+ naturalHeightRef.current = height;
373
+ setMeasuredContentHeight(height);
374
+ }
375
+ });
376
+ return () => observer.disconnect();
377
+ }, [isOpen, drawerContent, measurePeek, absoluteHeader, absoluteFooter, footerInsetAdjustment, measureContent, scrollableRef]);
225
378
 
226
- // Web mirror of native's scrollable handling for 'auto' detents: a plugged
227
- // scrollable keeps the sized (bounded) layout so its viewport is capped to
228
- // the visible sheet and can scroll, while the 'auto' height is measured with
229
- // the viewport replaced by the scrollable's content size — mirrors native
230
- // `naturalHeight`.
231
- const [hasBoundedScrollable, setHasBoundedScrollable] = useState(false);
232
- const [scrollableAutoHeight, setScrollableAutoHeight] = useState(0);
233
- const pinnedScrollerRef = useRef(null);
234
-
235
- // Natural content height (header + content + footer, with a pinned
236
- // scrollable's viewport replaced by its content size) — the height the
237
- // content wants regardless of the sheet's bounds.
238
- const measureNaturalHeight = useCallback(() => {
239
- const contentEl = getDOMElement(contentRef.current);
240
- if (!contentEl || !contentEl.isConnected) return 0;
241
- const headerEl = absoluteHeaderRef.current ? null : getDOMElement(headerElRef.current);
242
- const footerEl = absoluteFooterRef.current ? null : getDOMElement(footerElRef.current);
243
- let height = (headerEl?.offsetHeight ?? 0) + (footerEl?.offsetHeight ?? 0) + contentEl.offsetHeight;
244
- const scroller = pinnedScrollerRef.current;
245
- const scrollContent = scroller?.firstElementChild;
246
- if (scroller?.isConnected && scrollContent instanceof HTMLElement) {
247
- height += scrollContent.offsetHeight - scroller.clientHeight;
248
- }
249
- return Math.max(0, height);
250
- }, []);
379
+ // Re-reveal the focused field once the sheet settles above the keyboard
251
380
  useEffect(() => {
252
- if (!isOpen || !hasAutoDetent) return undefined;
381
+ const drawer = drawerContentRef.current;
382
+ if (!keyboardViewport || !drawer) return undefined;
253
383
  let canceled = false;
254
- let rafId = 0;
255
- let mutationObserver = null;
256
- let resizeObserver = null;
257
- const attach = () => {
258
- if (canceled) return;
259
- const drawerEl = drawerContentRef.current;
260
- if (!drawerEl || !drawerEl.isConnected) {
261
- // Radix Presence defers the portal mount; poll until the drawer is live.
262
- rafId = window.requestAnimationFrame(attach);
263
- return;
264
- }
265
- const measure = () => setScrollableAutoHeight(measureNaturalHeight());
266
- let observed = null;
267
-
268
- // (Re)pin the first vertical scrollable and observe the nodes whose size
269
- // feeds the natural height — content growth inside a bounded scroller is
270
- // invisible to the sheet's own layout (mirrors native's contentSize
271
- // observation). Resolves the content node live: the layout-branch swap
272
- // (natural flow ↔ sized) remounts it.
273
- const observe = () => {
274
- const contentEl = getDOMElement(contentRef.current);
275
- const headerEl = getDOMElement(headerElRef.current);
276
-
277
- // Skip mutations that don't change the observed topology — e.g. a
278
- // virtualized list mounting rows inside the pinned scroller, which
279
- // would otherwise trigger a full-subtree rescan (findVerticalScroller
280
- // resolves computed styles) on every scroll frame. Size changes are
281
- // already covered by the ResizeObserver.
282
- if (observed && observed.contentEl === contentEl && contentEl?.isConnected && observed.headerEl === headerEl && observed.scroller.isConnected && observed.scrollContent === observed.scroller.firstElementChild) {
283
- return;
284
- }
285
- const scroller = contentEl && contentEl.isConnected ? findVerticalScroller(contentEl) : null;
286
- pinnedScrollerRef.current = scroller;
287
- setHasBoundedScrollable(scroller != null);
288
- observed = scroller ? {
289
- contentEl,
290
- headerEl,
291
- scroller,
292
- scrollContent: scroller.firstElementChild
293
- } : null;
294
- resizeObserver?.disconnect();
295
- resizeObserver = new ResizeObserver(measure);
296
- if (contentEl?.isConnected) resizeObserver.observe(contentEl);
297
- if (headerEl) resizeObserver.observe(headerEl);
298
- if (scroller) {
299
- resizeObserver.observe(scroller);
300
- if (scroller.firstElementChild) resizeObserver.observe(scroller.firstElementChild);
301
- }
302
- measure();
303
- };
304
- observe();
305
- // Watch the whole drawer subtree — catches the scrollable mounting/
306
- // unmounting AND the layout-branch swap, which remounts the content node
307
- // (a content-scoped observer would go stale after the swap).
308
- mutationObserver = new MutationObserver(observe);
309
- mutationObserver.observe(drawerEl, {
310
- childList: true,
311
- subtree: true
312
- });
313
- };
314
- rafId = window.requestAnimationFrame(attach);
384
+ // Flush styles so this commit's snap transition shows up in getAnimations()
385
+ // eslint-disable-next-line no-void
386
+ void drawer.offsetHeight;
387
+ Promise.allSettled(drawer.getAnimations().map(a => a.finished)).then(() => {
388
+ if (!canceled) revealFocusedElement(drawer);
389
+ });
315
390
  return () => {
316
391
  canceled = true;
317
- window.cancelAnimationFrame(rafId);
318
- mutationObserver?.disconnect();
319
- resizeObserver?.disconnect();
320
392
  };
321
- }, [isOpen, hasAutoDetent, measureNaturalHeight]);
393
+ }, [keyboardViewport]);
322
394
 
323
395
  // Below the last detent a vertical touch pan moves the sheet, not the
324
396
  // content — `[data-vaul-scroll-locked]` disables vertical touch panning on
325
397
  // the scroll container and everything inside it (see vaul/style.css).
326
398
  const isScrollLocked = validDetents.length > 0 && activeSnapPoint !== validDetents[validDetents.length - 1];
327
-
328
- // Vaul measures the auto-size wrapper's offsetHeight (always, post fork).
329
- // Track it here so the form sheet can size its card to fit content,
330
- // clamped between a minimum ratio of the viewport and a maximum derived
331
- // from `detachedOffset` (the breathing room left at top + bottom of the
332
- // floating card).
333
- const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
334
399
  const effectiveMaxContentHeight = useMemo(() => {
335
400
  if (maxContentHeight !== undefined) return maxContentHeight;
336
401
  if (!isFormSheet) return undefined;
@@ -394,7 +459,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
394
459
  effectiveMaxContentHeight,
395
460
  peekHeight,
396
461
  peekContentHeight,
397
- measuredContentHeight
462
+ keyboardViewport
398
463
  });
399
464
  geometryInputsRef.current = {
400
465
  effectiveDetached,
@@ -402,20 +467,20 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
402
467
  effectiveMaxContentHeight,
403
468
  peekHeight,
404
469
  peekContentHeight,
405
- measuredContentHeight
470
+ keyboardViewport
406
471
  };
407
472
  const computeDetentGeometry = useCallback(() => {
408
473
  const inputs = geometryInputsRef.current;
409
- const windowH = window.innerHeight;
410
- const effectiveH = inputs.effectiveDetached ? windowH - inputs.effectiveDetachedOffset : windowH;
474
+ const windowH = inputs.keyboardViewport?.height ?? window.innerHeight;
475
+ const bottomGap = inputs.effectiveDetached ? inputs.effectiveDetachedOffset : 0;
476
+ const effectiveH = windowH - bottomGap;
477
+ // Above a keyboard, fractions keep their full-height size (see vaul).
478
+ const fractionH = inputs.keyboardViewport ? inputs.keyboardViewport.layoutHeight - bottomGap : effectiveH;
411
479
  // Matches vaul's height ceiling: min(effectiveH, maxContentHeight).
412
480
  const ceiling = inputs.effectiveMaxContentHeight !== undefined ? Math.min(effectiveH, inputs.effectiveMaxContentHeight) : effectiveH;
413
- // 'auto' resolves to the content's natural height. Read it live — the
414
- // `measuredContentHeight` state lags mount by a frame (vaul's initial
415
- // ref-callback measure runs while the portal subtree is still detached,
416
- // so it reads 0 until the ResizeObserver fires). Falls back to the state
417
- // value, then to vaul's pre-measure fallback (effectiveHeight / 2).
418
- const measuredHeight = measureNaturalHeight() || inputs.measuredContentHeight;
481
+ // Reuse the intrinsic measurement during position events so animation
482
+ // frames don't clone the content tree again.
483
+ const measuredHeight = naturalHeightRef.current;
419
484
  const autoHeight = Math.min(measuredHeight > 0 ? measuredHeight : effectiveH / 2, ceiling);
420
485
 
421
486
  // 'peek' is measured live from the DOM (offsetHeight is layout-based, so
@@ -435,7 +500,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
435
500
  const positions = [];
436
501
  const values = [];
437
502
  for (const d of validDetentsRef.current) {
438
- const h = typeof d === 'number' ? Math.min(d * effectiveH, ceiling) : d === 'peek' ? Math.min(livePeekHeight, ceiling) : autoHeight;
503
+ const h = typeof d === 'number' ? Math.min(d * fractionH, ceiling) : d === 'peek' ? Math.min(livePeekHeight, ceiling) : autoHeight;
439
504
  positions.push(effectiveH - h);
440
505
  values.push(effectiveH > 0 ? h / effectiveH : 0);
441
506
  }
@@ -446,7 +511,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
446
511
  positions,
447
512
  values
448
513
  };
449
- }, [measureNaturalHeight]);
514
+ }, []);
450
515
 
451
516
  // Detent info for lifecycle events. Position/detent come from the active
452
517
  // detent's target geometry — not the live DOM rect — so willPresent (drawer
@@ -533,50 +598,20 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
533
598
  detent: values[count - 1]
534
599
  };
535
600
  }, [computeDetentGeometry]);
536
-
537
- // Mirror native synchronous layout: while dragging (and through the post-
538
- // release settle) the sized layout tracks the sheet's visible height in
539
- // realtime, clamped to the smallest detent — dragging below it slides the
540
- // sheet out without resizing. Once settled, the inline height is cleared so
541
- // the CSS calc() owns sizing again (adapts to viewport resizes at rest).
542
601
  const sizedLayoutRef = useRef(null);
543
602
  const isDraggingRef = useRef(false);
544
- const isSettlingAfterDragRef = useRef(false);
545
- const updateSizedLayoutHeight = useCallback(position => {
546
- const node = sizedLayoutRef.current;
547
- if (!node) return;
548
- const {
549
- effectiveH,
550
- ceiling,
551
- positions
552
- } = computeDetentGeometry();
553
- if (positions.length === 0) return;
554
- if (isSettlingAfterDragRef.current) {
555
- const snap = activeSnapPointRef.current;
556
- const index = snap != null ? validDetentsRef.current.indexOf(snap) : -1;
557
- const target = index >= 0 ? positions[index] : null;
558
- if (target != null && Math.abs(position - target) < 1) {
559
- isSettlingAfterDragRef.current = false;
560
- // Restore the calc — height is an inline style, so clearing it would
561
- // leave the div unsized (React won't re-apply it without a render)
562
- node.style.height = SIZED_LAYOUT_HEIGHT;
563
- return;
564
- }
565
- }
566
-
567
- // positions[0] is the smallest detent's top-Y — its height is the clamp floor.
568
- const minHeight = effectiveH - positions[0];
569
- const height = Math.min(Math.max(effectiveH - position, minHeight), ceiling);
570
- node.style.height = `${height}px`;
571
- }, [computeDetentGeometry]);
572
603
  const handlePositionChange = useCallback(position => {
573
- if (isDraggingRef.current || isSettlingAfterDragRef.current) {
574
- updateSizedLayoutHeight(position);
575
- }
576
604
  const {
577
605
  index,
578
606
  detent
579
607
  } = interpolateFromPosition(position);
608
+ if (isOpenRef.current && !isPresentingRef.current) {
609
+ const {
610
+ effectiveH,
611
+ positions
612
+ } = computeDetentGeometry();
613
+ setDisplayedBackgroundIndex(current => getDetentBackgroundIndex(effectiveH - position, positions.map(top => effectiveH - top), current));
614
+ }
580
615
  onPositionChangeRef.current?.({
581
616
  nativeEvent: {
582
617
  index,
@@ -585,7 +620,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
585
620
  realtime: true
586
621
  }
587
622
  });
588
- }, [interpolateFromPosition, updateSizedLayoutHeight]);
623
+ }, [interpolateFromPosition, computeDetentGeometry]);
589
624
 
590
625
  // Fire onMount once after first render. React-mount is the earliest point
591
626
  // the component is ready for imperative calls, matching the native
@@ -611,6 +646,12 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
611
646
  if (isOpen === wasOpen) return undefined;
612
647
  const present = isOpen;
613
648
  if (!present) {
649
+ // Mirror native: dismissing a sheet takes every sheet stacked above it
650
+ // (iOS dismisses from the presenter; Android runs dismissStack first).
651
+ dismissDescendantsRef.current();
652
+ // Mirror native's lazy-mount cancel: dismissed before willPresent, no events
653
+ if (!didEmitWillPresentRef.current) return undefined;
654
+ didEmitWillPresentRef.current = false;
614
655
  // Pair willBlur with willDismiss on dismiss — mirrors native iOS
615
656
  // emitWillDismissEvents (blur fires before dismiss). willPresent is
616
657
  // deferred to `start()` below (needs the mounted drawer's geometry).
@@ -623,19 +664,29 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
623
664
  }
624
665
  const fireDone = () => {
625
666
  if (present) {
667
+ isPresentingRef.current = false;
668
+ // A dismiss may already be underway (it lands on the next commit)
669
+ if (phaseRef.current === 'presenting') phaseRef.current = 'presented';
626
670
  onDidPresentRef.current?.({
627
671
  nativeEvent: computeDetentInfo()
628
672
  });
629
673
  onDidFocusRef.current?.({
630
674
  nativeEvent: null
631
675
  });
676
+ settle(resolvePresentRef);
677
+ if (shouldDismissOnPresentRef.current) {
678
+ shouldDismissOnPresentRef.current = false;
679
+ setIsOpen(false);
680
+ }
632
681
  } else {
682
+ phaseRef.current = 'dismissed';
633
683
  onDidBlurRef.current?.({
634
684
  nativeEvent: null
635
685
  });
636
686
  onDidDismissRef.current?.({
637
687
  nativeEvent: null
638
688
  });
689
+ settle(resolveDismissRef);
639
690
  }
640
691
  };
641
692
  let canceled = false;
@@ -659,6 +710,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
659
710
  // willPresent mirrors native iOS where viewWillAppear dispatches
660
711
  // both; the descendant-stack focus effect handles subsequent
661
712
  // transitions.
713
+ didEmitWillPresentRef.current = true;
662
714
  onWillPresentRef.current?.({
663
715
  nativeEvent: computeDetentInfo()
664
716
  });
@@ -728,12 +780,9 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
728
780
  nativeEvent: computeDetentInfo(true)
729
781
  });
730
782
  }, [computeDetentInfo]);
731
- const handleRelease = useCallback((_event, open) => {
783
+ const handleRelease = useCallback(() => {
732
784
  if (!isDraggingRef.current) return;
733
785
  isDraggingRef.current = false;
734
- // Track the sized layout through the settle animation only when the sheet
735
- // stays open — a dismissal slides out at the min-clamped height.
736
- isSettlingAfterDragRef.current = open;
737
786
  onDragEndRef.current?.({
738
787
  nativeEvent: computeDetentInfo(true)
739
788
  });
@@ -742,12 +791,20 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
742
791
  isNested,
743
792
  dismissAbove,
744
793
  descendants
745
- } = useSheetStack(methodsRef, drawerContentRef, isOpen, isFormSheet);
794
+ } = useSheetStack(handleRef, drawerContentRef, isOpen, isFormSheet);
746
795
  dismissAboveRef.current = dismissAbove;
796
+ // Captured per render: by the time the dismiss effect runs, this sheet has
797
+ // already been popped from the stack, so `dismissAbove` can't find it.
798
+ dismissDescendantsRef.current = () => {
799
+ for (let i = descendants.length - 1; i >= 0; i--) {
800
+ descendants[i].ref.current?.dismissQuietly();
801
+ }
802
+ };
747
803
 
748
804
  // Mirror Android: translate this sheet down to match the deepest descendant's
749
805
  // top so the whole stack visually aligns. Cascades because every ancestor
750
806
  // re-runs whenever the stack (and thus its descendants) changes.
807
+ const wasCascadedRef = useRef(false);
751
808
  useEffect(() => {
752
809
  const parent = drawerContentRef.current;
753
810
  if (!parent) return;
@@ -758,7 +815,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
758
815
  // out — nothing to align with anymore.
759
816
  if (!isOpen) return;
760
817
  const parentWrapper = parent.closest('[data-vaul-detached-wrapper]');
761
- const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
818
+ const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')}), --snap-point-height ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
762
819
  const wrapperTransition = `clip-path ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
763
820
  const CLIP_NONE = 'inset(0px round 0px)';
764
821
 
@@ -781,22 +838,29 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
781
838
  parentWrapper.style.clipPath = next;
782
839
  };
783
840
  if (descendants.length === 0) {
841
+ if (!wasCascadedRef.current) return;
842
+ wasCascadedRef.current = false;
784
843
  parent.style.transition = transition;
785
844
  parent.style.transform = '';
786
845
  setClip(CLIP_NONE);
787
846
  return;
788
847
  }
848
+ wasCascadedRef.current = true;
789
849
 
790
- // Track only the immediate child's snap point. Walking deeper descendants
791
- // would push this sheet further when a grandchild opens, even when our
792
- // own child didn't move (e.g., child skipped its cascade for a page
793
- // grandchild) — leaving a visible gap between this sheet and its child.
850
+ // Track the immediate child's *actual* top: its cascade transform when it
851
+ // was itself pushed behind a grandchild, else its own snap point. Mirrors
852
+ // Android's addTranslation propagating extra translation up the stack —
853
+ // a grandchild opening pushes this sheet only as far as our child moved.
854
+ const readTranslateY = el => {
855
+ const match = el.style.transform.match(/translate3d\([^,]*,\s*(-?\d*\.?\d+)px/);
856
+ return match ? Number.parseFloat(match[1]) : null;
857
+ };
794
858
  const computeTargetY = () => {
795
859
  const parentSnap = Number.parseFloat(parent.style.getPropertyValue('--snap-point-height')) || 0;
796
860
  const node = descendants[0]?.nodeRef.current;
797
861
  if (!node) return parentSnap;
798
- const childSnap = Number.parseFloat(node.style.getPropertyValue('--snap-point-height')) || 0;
799
- return Math.max(parentSnap, childSnap);
862
+ const childTop = readTranslateY(node) ?? (Number.parseFloat(node.style.getPropertyValue('--snap-point-height')) || 0);
863
+ return Math.max(parentSnap, childTop);
800
864
  };
801
865
 
802
866
  // When a form-sheet parent has a form-sheet descendant, clip the parent to
@@ -830,18 +894,26 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
830
894
  };
831
895
  const apply = () => {
832
896
  applyFormClip();
833
- // Mirror iOS: a page-sheet child fully covers a form-sheet parent, so the
834
- // cascade push-down has no visible effect — and would briefly peek above
835
- // the page during the present animation. Leave the parent put.
836
- const child = descendants[0];
837
- if (isFormSheet && child && !child.isFormSheetRef.current) {
897
+ // Mirror iPad/Android (`isExpanded`): a parent at full height (detent 1)
898
+ // stays put and the child simply overlays it. Full height is the sheet's
899
+ // own ceiling — the viewport for a page sheet, the capped content-fit
900
+ // height for a form sheet — so compare against where detent 1 lands.
901
+ const parentSnap = Number.parseFloat(parent.style.getPropertyValue('--snap-point-height')) || 0;
902
+ const {
903
+ effectiveH,
904
+ ceiling
905
+ } = computeDetentGeometry();
906
+ if (parentSnap <= effectiveH - ceiling + 0.5) {
838
907
  parent.style.transition = transition;
839
908
  parent.style.transform = '';
840
909
  return;
841
910
  }
911
+ // Mirror Android: stay put while the child is being dragged (its live
912
+ // transform would drag us along during drag-to-dismiss). The release
913
+ // snap rewrites the child's style and re-applies.
914
+ if (descendants[0]?.nodeRef.current?.classList.contains(DRAG_CLASS)) return;
842
915
  const targetY = computeTargetY();
843
- const match = parent.style.transform.match(/translate3d\([^,]*,\s*(-?\d*\.?\d+)px/);
844
- const currentY = match ? Number.parseFloat(match[1]) : 0;
916
+ const currentY = readTranslateY(parent) ?? 0;
845
917
  if (Math.abs(currentY - targetY) < 0.5) return;
846
918
  parent.style.transition = transition;
847
919
  parent.style.transform = `translate3d(0, ${targetY}px, 0)`;
@@ -855,11 +927,17 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
855
927
  attributes: true,
856
928
  attributeFilter: ['style']
857
929
  });
930
+ // The child's own cascade lands in a later frame — follow it when it moves.
931
+ const childNode = descendants[0]?.nodeRef.current;
932
+ if (childNode) observer.observe(childNode, {
933
+ attributes: true,
934
+ attributeFilter: ['style']
935
+ });
858
936
  return () => {
859
937
  cancelAnimationFrame(raf);
860
938
  observer.disconnect();
861
939
  };
862
- }, [descendants, activeSnapPoint, cornerRadius, isFormSheet, isOpen]);
940
+ }, [descendants, activeSnapPoint, cornerRadius, isFormSheet, isOpen, computeDetentGeometry]);
863
941
 
864
942
  // Focus/blur events fire when a descendant sheet appears on top of this one
865
943
  // (blur) or when all descendants are dismissed (focus). will-events fire
@@ -870,12 +948,19 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
870
948
  const onDidBlurRef = useRef(onDidBlur);
871
949
  const onWillFocusRef = useRef(onWillFocus);
872
950
  const onDidFocusRef = useRef(onDidFocus);
951
+ const onDismissAttemptRef = useRef(onDismissAttempt);
873
952
  useEffect(() => {
874
953
  onWillBlurRef.current = onWillBlur;
875
954
  onDidBlurRef.current = onDidBlur;
876
955
  onWillFocusRef.current = onWillFocus;
877
956
  onDidFocusRef.current = onDidFocus;
957
+ onDismissAttemptRef.current = onDismissAttempt;
878
958
  });
959
+ const handleDismissAttempt = useCallback(() => {
960
+ onDismissAttemptRef.current?.({
961
+ nativeEvent: null
962
+ });
963
+ }, []);
879
964
  const prevDescendantCountRef = useRef(0);
880
965
  useEffect(() => {
881
966
  const prevCount = prevDescendantCountRef.current;
@@ -924,11 +1009,6 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
924
1009
  window.cancelAnimationFrame(rafId);
925
1010
  };
926
1011
  }, [isOpen, descendants.length]);
927
-
928
- // Definite-height flex layout (per-detent sizing) unless content must be
929
- // measured in natural flow: 'auto' detents (without a plugged scrollable —
930
- // those measure via `measureNaturalHeight`) and form-sheet content-fit sizing.
931
- const useSizedLayout = (!hasAutoDetent || hasBoundedScrollable) && !isFormSheet;
932
1012
  const effectiveCornerRadius = cornerRadius ?? DEFAULT_CORNER_RADIUS;
933
1013
 
934
1014
  // Shadow cast upward from the sheet's top edge toward the background. Matches
@@ -942,6 +1022,23 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
942
1022
  // wrapper edges). drop-shadow on the wrapper follows the post-clip silhouette
943
1023
  // and isn't clipped by the wrapper itself.
944
1024
  const dropShadow = elevation > 0 ? `drop-shadow(0 ${-elevation}px ${elevation * 3}px rgba(0, 0, 0, 0.15))` : undefined;
1025
+ const backgroundLayerStyle = {
1026
+ position: 'absolute',
1027
+ inset: 0,
1028
+ pointerEvents: 'none',
1029
+ backgroundColor: backgroundColor,
1030
+ transition: isPresentingRef.current ? 'none' : 'background-color 200ms'
1031
+ };
1032
+
1033
+ // Vaul fades the overlay through its opacity, so the dim opacity goes into the color
1034
+ const resolvedOverlayStyle = useMemo(() => {
1035
+ if (dimColor == null && dimOpacity == null) return overlayStyle;
1036
+ const percentage = Math.min(1, Math.max(0, dimOpacity ?? DEFAULT_DIM_OPACITY)) * 100;
1037
+ return {
1038
+ ...overlayStyle,
1039
+ backgroundColor: `color-mix(in srgb, ${String(dimColor ?? 'black')} ${percentage}%, transparent)`
1040
+ };
1041
+ }, [dimColor, dimOpacity]);
945
1042
  const mergedContentStyle = useMemo(() => ({
946
1043
  position: 'fixed',
947
1044
  top: 0,
@@ -952,7 +1049,6 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
952
1049
  flexDirection: 'column',
953
1050
  borderTopLeftRadius: effectiveCornerRadius,
954
1051
  borderTopRightRadius: effectiveCornerRadius,
955
- backgroundColor: backgroundColor,
956
1052
  // Clip children to the rounded top so headers/content with their own
957
1053
  // background don't bleed past the corners. `clip` (not `hidden`): a
958
1054
  // hidden box is still a scroll container, so browser focus-reveal (e.g.
@@ -962,8 +1058,11 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
962
1058
  // Lift content above iOS home indicator / bottom safe area when enabled.
963
1059
  // A relative footer owns the inset instead (see resolvedFooterStyle); an
964
1060
  // absolute footer floats over the content, so the content keeps its lift.
965
- paddingBottom: insetAdjustment === 'automatic' && !(footerOwnsInset && !absoluteFooter) ? 'env(safe-area-inset-bottom, 0px)' : 0
966
- }), [backgroundColor, effectiveCornerRadius, insetAdjustment, footerOwnsInset, absoluteFooter]);
1061
+ // A plugged scrollable keeps the lift too (contentInsetAdjustment
1062
+ // 'automatic' | 'safe-area') — 'footer' | 'never' scroll edge-to-edge
1063
+ // behind the indicator with user-applied content padding.
1064
+ paddingBottom: insetAdjustment === 'automatic' && !(footerOwnsInset && !absoluteFooter) && (!scrollableRef || insetBehavior === 'automatic' || insetBehavior === 'safe-area') ? 'env(safe-area-inset-bottom, 0px)' : 0
1065
+ }), [effectiveCornerRadius, insetAdjustment, footerOwnsInset, absoluteFooter, scrollableRef, insetBehavior]);
967
1066
  const defaultGrabberColor = colorScheme === 'dark' ? DEFAULT_GRABBER_COLOR_DARK : DEFAULT_GRABBER_COLOR_LIGHT;
968
1067
  const grabberHeight = grabberOptions?.height ?? DEFAULT_GRABBER_HEIGHT;
969
1068
 
@@ -994,7 +1093,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
994
1093
  // `maxContentWidth` is ignored ('form' is absolute).
995
1094
  // - presentation='page' → `maxContentWidth` (any viewport) or
996
1095
  // DEFAULT_MAX_WIDTH (tablet/landscape readability cap).
997
- // Detached without a width constraint applies anchorOffset on both edges so
1096
+ // Detached without a width constraint applies placementOffset on both edges so
998
1097
  // the floating card breathes from the viewport sides.
999
1098
  const wrapperStyle = useMemo(() => {
1000
1099
  // Mobile portrait ignores width sizing entirely (matches iOS/Android:
@@ -1006,23 +1105,23 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
1006
1105
  const next = {};
1007
1106
  if (dropShadow) next.filter = dropShadow;
1008
1107
  if (!needsMargins) return next;
1009
- let marginLeft;
1010
- let marginRight;
1108
+ let marginInlineStart;
1109
+ let marginInlineEnd;
1011
1110
  if (isFormSheet) {
1012
- marginLeft = 'auto';
1013
- marginRight = 'auto';
1111
+ marginInlineStart = 'auto';
1112
+ marginInlineEnd = 'auto';
1014
1113
  } else if (maxWidth == null) {
1015
- marginLeft = anchorOffset;
1016
- marginRight = anchorOffset;
1114
+ marginInlineStart = placementOffset;
1115
+ marginInlineEnd = placementOffset;
1017
1116
  } else {
1018
- marginLeft = anchor === 'left' ? anchorOffset : 'auto';
1019
- marginRight = anchor === 'right' ? anchorOffset : 'auto';
1117
+ marginInlineStart = placement === 'leading' ? placementOffset : 'auto';
1118
+ marginInlineEnd = placement === 'trailing' ? placementOffset : 'auto';
1020
1119
  }
1021
1120
  if (maxWidth != null) next.maxWidth = maxWidth;
1022
- next.marginLeft = marginLeft;
1023
- next.marginRight = marginRight;
1121
+ next.marginInlineStart = marginInlineStart;
1122
+ next.marginInlineEnd = marginInlineEnd;
1024
1123
  return next;
1025
- }, [isLandscapeOrTablet, isFormSheet, maxContentWidth, presentation, anchor, anchorOffset, effectiveDetached, dropShadow]);
1124
+ }, [isLandscapeOrTablet, isFormSheet, maxContentWidth, presentation, placement, placementOffset, effectiveDetached, dropShadow]);
1026
1125
 
1027
1126
  // Absolute-position the grabber so it overlays the content top-edge
1028
1127
  // instead of consuming flow height — mirrors native iOS/Android, where
@@ -1052,6 +1151,8 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
1052
1151
  onDrag: handleDrag,
1053
1152
  onRelease: handleRelease,
1054
1153
  dismissible: dismissible,
1154
+ dismissThreshold: dismissThreshold,
1155
+ onDismissAttempt: handleDismissAttempt,
1055
1156
  draggable: draggable,
1056
1157
  repositionInputs: false,
1057
1158
  modal: dimmed,
@@ -1061,22 +1162,19 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
1061
1162
  detachedRadius: effectiveCornerRadius,
1062
1163
  maxContentHeight: effectiveMaxContentHeight,
1063
1164
  peekHeight: peekHeight,
1064
- initialAnimated: initialDetentAnimated,
1165
+ initialAnimated: shouldAutoPresent ? initialDetentAnimated : true,
1065
1166
  detachedWrapperStyle: wrapperStyle,
1066
- onContentHeightChange: setMeasuredContentHeight,
1067
- contentHeight:
1068
- // Bounded scrollable → the auto-size wrapper measures 0 (sized
1069
- // layout is absolute), so feed vaul the natural height instead.
1070
- hasAutoDetent && hasBoundedScrollable && scrollableAutoHeight > 0 ? scrollableAutoHeight : undefined,
1167
+ contentHeight: measuredContentHeight,
1168
+ keyboardViewport: keyboardViewport,
1071
1169
  activeSnapPoint: activeSnapPoint,
1072
1170
  setActiveSnapPoint: handleSetActiveSnapPoint,
1073
1171
  ...snapPointsProps,
1074
1172
  children: /*#__PURE__*/_jsxs(Drawer.Portal, {
1075
1173
  container: portalContainer ?? undefined,
1076
1174
  children: [/*#__PURE__*/_jsx(Drawer.Overlay, {
1077
- style: overlayStyle
1175
+ style: resolvedOverlayStyle
1078
1176
  }), /*#__PURE__*/_jsxs(Drawer.Content, {
1079
- ref: drawerContentRef,
1177
+ ref: handleDrawerRef,
1080
1178
  style: mergedContentStyle,
1081
1179
  onPointerDownOutside: handlePointerDownOutside,
1082
1180
  detachedSiblings: footer && absoluteFooter ? /*#__PURE__*/_jsx("div", {
@@ -1088,31 +1186,30 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
1088
1186
  children: /*#__PURE__*/isValidElement(footer) ? footer : /*#__PURE__*/createElement(footer)
1089
1187
  })
1090
1188
  }) : undefined,
1091
- children: [/*#__PURE__*/_jsx(Drawer.Title, {
1189
+ children: [/*#__PURE__*/_jsx("div", {
1190
+ "aria-hidden": true,
1191
+ style: backgroundLayerStyle
1192
+ }), /*#__PURE__*/_jsx(Drawer.Title, {
1092
1193
  style: visuallyHiddenStyle,
1093
1194
  children: accessibilityOptions?.paneTitle ?? 'Sheet'
1094
1195
  }), grabber && /*#__PURE__*/_jsx(Drawer.Handle, {
1095
1196
  style: handleStyle
1096
- }), useSizedLayout ?
1097
- /*#__PURE__*/
1098
- // vaul wraps children in `[data-vaul-auto-size-wrapper]` (display:
1099
- // flow-root) which doesn't honor descendant flex layout. Use an
1100
- // absolute fill sized to the visible portion (via vaul's
1101
- // `--snap-point-height` var) so the inner flex column has a
1102
- // definite height for flex layouts to fill — mirrors native, where
1103
- // the container is sized to the sheet's visible height per detent.
1104
- _jsxs("div", {
1197
+ }), /*#__PURE__*/_jsxs("div", {
1105
1198
  ref: sizedLayoutRef,
1106
1199
  style: sizedLayoutStyle,
1107
1200
  "data-vaul-scroll-locked": isScrollLocked ? '' : undefined,
1108
- children: [header && /*#__PURE__*/_jsx(View, {
1201
+ children: [/*#__PURE__*/_jsx(TrueSheetBackground, {
1202
+ background: background,
1203
+ backgroundStyle: backgroundStyle
1204
+ }), header && /*#__PURE__*/_jsx(View, {
1109
1205
  ref: headerElRef,
1110
1206
  style: resolvedHeaderStyle,
1111
1207
  onLayout: handleHeaderLayout,
1112
1208
  children: /*#__PURE__*/isValidElement(header) ? header : /*#__PURE__*/createElement(header)
1113
1209
  }), /*#__PURE__*/_jsx(View, {
1114
1210
  ref: contentRef,
1115
- style: hasAutoDetent && hasBoundedScrollable ? [contentFillStyle, style] : style,
1211
+ style: [contentStyle, style],
1212
+ onLayout: handleContentLayout,
1116
1213
  children: children
1117
1214
  }), footer && !absoluteFooter && /*#__PURE__*/_jsx(View, {
1118
1215
  ref: footerElRef,
@@ -1120,32 +1217,13 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
1120
1217
  onLayout: handleFooterLayout,
1121
1218
  children: /*#__PURE__*/isValidElement(footer) ? footer : /*#__PURE__*/createElement(footer)
1122
1219
  })]
1123
- }) :
1124
- /*#__PURE__*/
1125
- // Natural flow so vaul can measure content height — required for
1126
- // 'auto' detents and form-sheet content-fit sizing.
1127
- _jsxs(_Fragment, {
1128
- children: [header && /*#__PURE__*/_jsx(View, {
1129
- ref: headerElRef,
1130
- style: resolvedHeaderStyle,
1131
- onLayout: handleHeaderLayout,
1132
- children: /*#__PURE__*/isValidElement(header) ? header : /*#__PURE__*/createElement(header)
1133
- }), /*#__PURE__*/_jsx(View, {
1134
- ref: contentRef,
1135
- style: style,
1136
- children: children
1137
- }), footer && !absoluteFooter && /*#__PURE__*/_jsx(View, {
1138
- ref: footerElRef,
1139
- style: resolvedFooterStyle,
1140
- onLayout: handleFooterLayout,
1141
- children: /*#__PURE__*/isValidElement(footer) ? footer : /*#__PURE__*/createElement(footer)
1142
- })]
1143
1220
  })]
1144
1221
  })]
1145
1222
  })
1146
1223
  })
1147
1224
  });
1148
1225
  });
1226
+ const DEFAULT_DIM_OPACITY = 0.5;
1149
1227
  const overlayStyle = {
1150
1228
  position: 'fixed',
1151
1229
  inset: 0,
@@ -1173,10 +1251,12 @@ const sizedLayoutStyle = {
1173
1251
  right: 0,
1174
1252
  height: SIZED_LAYOUT_HEIGHT,
1175
1253
  display: 'flex',
1176
- flexDirection: 'column'
1254
+ flexDirection: 'column',
1255
+ boxSizing: 'border-box',
1256
+ paddingBottom: 'inherit'
1177
1257
  };
1178
- const contentFillStyle = {
1179
- flex: 1,
1258
+ const contentStyle = {
1259
+ flexShrink: 1,
1180
1260
  minHeight: 0
1181
1261
  };
1182
1262
  const visuallyHiddenStyle = {
@@ -1190,6 +1270,15 @@ const visuallyHiddenStyle = {
1190
1270
  whiteSpace: 'nowrap',
1191
1271
  border: 0
1192
1272
  };
1273
+ const settle = resolveRef => {
1274
+ const resolve = resolveRef.current;
1275
+ resolveRef.current = null;
1276
+ resolve?.();
1277
+ };
1278
+
1279
+ // Same as native
1280
+ const ALREADY_PRESENTED_WARNING = 'TrueSheet: sheet is already presented. Use resize() to change detent.';
1281
+ const ALREADY_DISMISSED_WARNING = 'TrueSheet: sheet is already dismissed. No need to dismiss it again.';
1193
1282
  const STATIC_METHOD_ERROR = 'Static methods are not supported on web. Use the useTrueSheet() hook instead.';
1194
1283
  export const TrueSheet = TrueSheetComponent;
1195
1284
  const rejectStatic = async () => {