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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/README.md +25 -4
  2. package/android/build.gradle +22 -1
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +50 -16
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +302 -184
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +5 -1
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +21 -110
  7. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +116 -0
  8. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayViewManager.kt +45 -0
  9. package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +2 -1
  10. package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +18 -0
  11. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +119 -50
  12. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +360 -163
  13. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +54 -12
  14. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +22 -9
  15. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +84 -5
  16. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +104 -39
  17. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +3 -0
  18. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetOverlayRootView.kt +137 -0
  19. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +15 -0
  20. package/android/src/main/java/com/lodev09/truesheet/utils/KeyboardUtils.kt +15 -2
  21. package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +0 -12
  22. package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +19 -7
  23. package/android/src/main/jni/TrueSheetSpec.h +1 -0
  24. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +1 -9
  25. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +110 -30
  26. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +30 -7
  27. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +2 -2
  28. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +9 -8
  29. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h +3 -5
  30. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +9 -3
  31. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +3 -6
  32. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +1 -1
  33. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +4 -6
  34. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.cpp +1 -1
  35. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.h +1 -1
  36. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetLayoutUtils.h +43 -0
  37. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewComponentDescriptor.h +21 -0
  38. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.cpp +17 -0
  39. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.h +33 -0
  40. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h +3 -5
  41. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +7 -40
  42. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.h +3 -7
  43. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.cpp +3 -5
  44. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.h +4 -5
  45. package/ios/TrueSheetContainerView.h +27 -7
  46. package/ios/TrueSheetContainerView.mm +31 -11
  47. package/ios/TrueSheetContentView.h +43 -15
  48. package/ios/TrueSheetContentView.mm +211 -189
  49. package/ios/TrueSheetFooterView.h +6 -0
  50. package/ios/TrueSheetFooterView.mm +93 -17
  51. package/ios/TrueSheetModule.h +2 -7
  52. package/ios/TrueSheetModule.mm +7 -16
  53. package/ios/TrueSheetOverlayView.h +32 -0
  54. package/ios/TrueSheetOverlayView.mm +161 -0
  55. package/ios/TrueSheetView.mm +212 -55
  56. package/ios/TrueSheetViewController.h +31 -3
  57. package/ios/TrueSheetViewController.mm +393 -94
  58. package/ios/core/TrueSheetBlurView.h +0 -2
  59. package/ios/core/TrueSheetBlurView.mm +3 -36
  60. package/ios/core/TrueSheetDetentCalculator.h +7 -1
  61. package/ios/core/TrueSheetDetentCalculator.mm +27 -4
  62. package/ios/core/TrueSheetGrabberView.mm +13 -3
  63. package/ios/core/TrueSheetKeyboardObserver.mm +2 -0
  64. package/ios/core/TrueSheetOverlayContainerView.h +31 -0
  65. package/ios/core/TrueSheetOverlayContainerView.mm +35 -0
  66. package/ios/events/TrueSheetLifecycleEvents.h +2 -0
  67. package/ios/events/TrueSheetLifecycleEvents.mm +8 -0
  68. package/ios/tvos/TrueSheetStubs.mm +12 -0
  69. package/ios/utils/UIView+ScrollEdgeInteraction.h +1 -0
  70. package/ios/utils/UIView+ScrollEdgeInteraction.mm +24 -2
  71. package/lib/module/TrueSheet.js +173 -39
  72. package/lib/module/TrueSheet.js.map +1 -1
  73. package/lib/module/TrueSheet.web.js +218 -243
  74. package/lib/module/TrueSheet.web.js.map +1 -1
  75. package/lib/module/TrueSheetBackground.js +15 -0
  76. package/lib/module/TrueSheetBackground.js.map +1 -0
  77. package/lib/module/TrueSheetOverlay.js +30 -0
  78. package/lib/module/TrueSheetOverlay.js.map +1 -0
  79. package/lib/module/TrueSheetOverlay.web.js +35 -0
  80. package/lib/module/TrueSheetOverlay.web.js.map +1 -0
  81. package/lib/module/TrueSheetPeek.web.js +20 -15
  82. package/lib/module/TrueSheetPeek.web.js.map +1 -1
  83. package/lib/module/detentBackgrounds.js +21 -0
  84. package/lib/module/detentBackgrounds.js.map +1 -0
  85. package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  86. package/lib/module/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  87. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +14 -10
  88. package/lib/module/index.js +1 -0
  89. package/lib/module/index.js.map +1 -1
  90. package/lib/module/mocks/index.js +18 -2
  91. package/lib/module/mocks/index.js.map +1 -1
  92. package/lib/module/mocks/navigationExpoRouter.js.map +1 -1
  93. package/lib/module/navigation/base-types.js +2 -0
  94. package/lib/module/navigation/base-types.js.map +1 -0
  95. package/lib/module/navigation/createTrueSheetRouter.js +0 -9
  96. package/lib/module/navigation/createTrueSheetRouter.js.map +1 -1
  97. package/lib/module/navigation/expo-router/base-types.js +2 -0
  98. package/lib/module/navigation/expo-router/base-types.js.map +1 -0
  99. package/lib/module/navigation/expo-router/index.js +10 -7
  100. package/lib/module/navigation/expo-router/index.js.map +1 -1
  101. package/lib/module/navigation/expo-router/types.js +4 -0
  102. package/lib/module/navigation/expo-router/types.js.map +1 -0
  103. package/lib/module/navigation/index.js.map +1 -1
  104. package/lib/module/navigation/screen/useSheetScreenState.js +1 -0
  105. package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -1
  106. package/lib/module/web/constants.js +1 -1
  107. package/lib/module/web/constants.js.map +1 -1
  108. package/lib/module/web/layout.js +168 -0
  109. package/lib/module/web/layout.js.map +1 -0
  110. package/lib/module/web/vaul/constants.js +4 -0
  111. package/lib/module/web/vaul/constants.js.map +1 -1
  112. package/lib/module/web/vaul/context.js.map +1 -1
  113. package/lib/module/web/vaul/helpers.js +1 -0
  114. package/lib/module/web/vaul/helpers.js.map +1 -1
  115. package/lib/module/web/vaul/index.js +44 -6
  116. package/lib/module/web/vaul/index.js.map +1 -1
  117. package/lib/module/web/vaul/use-snap-points.js +6 -8
  118. package/lib/module/web/vaul/use-snap-points.js.map +1 -1
  119. package/lib/typescript/src/TrueSheet.d.ts +6 -0
  120. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  121. package/lib/typescript/src/TrueSheet.types.d.ts +135 -34
  122. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  123. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  124. package/lib/typescript/src/TrueSheetBackground.d.ts +3 -0
  125. package/lib/typescript/src/TrueSheetBackground.d.ts.map +1 -0
  126. package/lib/typescript/src/TrueSheetOverlay.d.ts +8 -0
  127. package/lib/typescript/src/TrueSheetOverlay.d.ts.map +1 -0
  128. package/lib/typescript/src/TrueSheetOverlay.web.d.ts +8 -0
  129. package/lib/typescript/src/TrueSheetOverlay.web.d.ts.map +1 -0
  130. package/lib/typescript/src/TrueSheetPeek.web.d.ts +8 -4
  131. package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
  132. package/lib/typescript/src/detentBackgrounds.d.ts +10 -0
  133. package/lib/typescript/src/detentBackgrounds.d.ts.map +1 -0
  134. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts +6 -0
  135. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts.map +1 -0
  136. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +11 -7
  137. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  138. package/lib/typescript/src/index.d.ts +1 -0
  139. package/lib/typescript/src/index.d.ts.map +1 -1
  140. package/lib/typescript/src/mocks/index.d.ts +4 -0
  141. package/lib/typescript/src/mocks/index.d.ts.map +1 -1
  142. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +3 -3
  143. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -1
  144. package/lib/typescript/src/navigation/base-types.d.ts +9 -0
  145. package/lib/typescript/src/navigation/base-types.d.ts.map +1 -0
  146. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +2 -13
  147. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -1
  148. package/lib/typescript/src/navigation/expo-router/base-types.d.ts +8 -0
  149. package/lib/typescript/src/navigation/expo-router/base-types.d.ts.map +1 -0
  150. package/lib/typescript/src/navigation/expo-router/index.d.ts +8 -6
  151. package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -1
  152. package/lib/typescript/src/navigation/expo-router/types.d.ts +44 -0
  153. package/lib/typescript/src/navigation/expo-router/types.d.ts.map +1 -0
  154. package/lib/typescript/src/navigation/index.d.ts +3 -4
  155. package/lib/typescript/src/navigation/index.d.ts.map +1 -1
  156. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +2 -1
  157. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -1
  158. package/lib/typescript/src/navigation/types.d.ts +41 -5
  159. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  160. package/lib/typescript/src/web/constants.d.ts +1 -1
  161. package/lib/typescript/src/web/constants.d.ts.map +1 -1
  162. package/lib/typescript/src/web/layout.d.ts +19 -0
  163. package/lib/typescript/src/web/layout.d.ts.map +1 -0
  164. package/lib/typescript/src/web/vaul/constants.d.ts +1 -0
  165. package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -1
  166. package/lib/typescript/src/web/vaul/context.d.ts +1 -0
  167. package/lib/typescript/src/web/vaul/context.d.ts.map +1 -1
  168. package/lib/typescript/src/web/vaul/helpers.d.ts +1 -0
  169. package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
  170. package/lib/typescript/src/web/vaul/index.d.ts +7 -1
  171. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
  172. package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
  173. package/package.json +17 -8
  174. package/react-native.config.js +1 -0
  175. package/src/TrueSheet.tsx +206 -48
  176. package/src/TrueSheet.types.ts +142 -35
  177. package/src/TrueSheet.web.tsx +270 -290
  178. package/src/TrueSheetBackground.tsx +17 -0
  179. package/src/TrueSheetOverlay.tsx +24 -0
  180. package/src/TrueSheetOverlay.web.tsx +33 -0
  181. package/src/TrueSheetPeek.web.tsx +25 -23
  182. package/src/detentBackgrounds.ts +24 -0
  183. package/src/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  184. package/src/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  185. package/src/fabric/TrueSheetViewNativeComponent.ts +14 -10
  186. package/src/index.ts +1 -0
  187. package/src/mocks/index.ts +17 -2
  188. package/src/mocks/navigationExpoRouter.ts +6 -5
  189. package/src/navigation/base-types.ts +20 -0
  190. package/src/navigation/createTrueSheetRouter.ts +5 -33
  191. package/src/navigation/expo-router/base-types.ts +18 -0
  192. package/src/navigation/expo-router/index.ts +28 -30
  193. package/src/navigation/expo-router/types.ts +116 -0
  194. package/src/navigation/index.ts +6 -7
  195. package/src/navigation/screen/useSheetScreenState.ts +2 -0
  196. package/src/navigation/types.ts +75 -22
  197. package/src/web/constants.ts +1 -1
  198. package/src/web/layout.ts +196 -0
  199. package/src/web/vaul/constants.ts +4 -0
  200. package/src/web/vaul/context.ts +1 -0
  201. package/src/web/vaul/helpers.ts +5 -0
  202. package/src/web/vaul/index.tsx +67 -5
  203. package/src/web/vaul/use-snap-points.ts +6 -14
@@ -5,23 +5,24 @@ import { createElement, forwardRef, isValidElement, useCallback, useEffect, useI
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 { observeSheetLayout } from "./web/layout.js";
10
13
  import { Drawer } from "./web/vaul/index.js";
11
- import { DEFAULT_PEEK_HEIGHT, TRANSITIONS } from "./web/vaul/constants.js";
14
+ import { DEFAULT_PEEK_HEIGHT, DRAG_CLASS, TRANSITIONS } from "./web/vaul/constants.js";
12
15
 
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;
16
+ // Resolves the user's `scrollableRef` to its scrollable DOM element — RN-web
17
+ // ScrollView refs expose the scroll node directly (with helpers attached);
18
+ // list refs (e.g. FlatList) expose it via getScrollableNode(). The node's
19
+ // first child is the content container.
20
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
21
+ const getScrollableElement = scrollable => {
22
+ if (!scrollable) return null;
23
+ if (scrollable instanceof HTMLElement) return scrollable;
24
+ const node = scrollable.getScrollableNode?.();
25
+ return node instanceof HTMLElement ? node : null;
25
26
  };
26
27
  const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
27
28
  const {
@@ -32,10 +33,15 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
32
33
  cornerRadius,
33
34
  style,
34
35
  backgroundColor: backgroundColorProp,
36
+ detentBackgrounds,
37
+ background,
38
+ backgroundStyle,
35
39
  maxContentHeight,
36
40
  maxContentWidth,
37
- anchor = 'center',
38
- anchorOffset = DEFAULT_ANCHOR_OFFSET,
41
+ placement = 'automatic',
42
+ placementOffset = DEFAULT_PLACEMENT_OFFSET,
43
+ scrollableRef,
44
+ scrollableOptions,
39
45
  grabber = true,
40
46
  grabberOptions,
41
47
  accessibilityOptions,
@@ -60,6 +66,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
60
66
  onDidPresent,
61
67
  onWillDismiss,
62
68
  onDidDismiss,
69
+ onDismissAttempt,
63
70
  onDetentChange,
64
71
  onDragBegin,
65
72
  onDragChange,
@@ -91,9 +98,13 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
91
98
  // is explicitly set.
92
99
  const effectiveDetached = isFormSheet || detached;
93
100
  const colorScheme = useColorScheme();
94
- const backgroundColor = backgroundColorProp ?? (colorScheme === 'dark' ? COLOR_SURFACE_CONTAINER_LOW_DARK : COLOR_SURFACE_CONTAINER_LOW_LIGHT);
95
101
  const shouldAutoPresent = initialDetentIndex >= 0 && initialDetentIndex < validDetents.length;
96
102
  const [isOpen, setIsOpen] = useState(shouldAutoPresent);
103
+ const isOpenRef = useRef(isOpen);
104
+ isOpenRef.current = isOpen;
105
+ const isPresentingRef = useRef(shouldAutoPresent);
106
+ const [displayedBackgroundIndex, setDisplayedBackgroundIndex] = useState(shouldAutoPresent ? initialDetentIndex : 0);
107
+ 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
108
  const [activeSnapPoint, setActiveSnapPoint] = useState(() => validDetents[shouldAutoPresent ? initialDetentIndex : 0] ?? null);
98
109
 
99
110
  // Keep activeSnapPoint valid if detents change (e.g., prop updates).
@@ -103,7 +114,6 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
103
114
  }, [validDetents]);
104
115
  const validDetentsRef = useRef(validDetents);
105
116
  validDetentsRef.current = validDetents;
106
- const hasAutoDetent = validDetents.includes('auto');
107
117
  const handleSetActiveSnapPoint = useCallback(snapPoint => {
108
118
  setActiveSnapPoint(snapPoint == null ? null : typeof snapPoint === 'number' || snapPoint === 'auto' || snapPoint === 'peek' ? snapPoint : null);
109
119
  }, []);
@@ -122,24 +132,43 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
122
132
  e.preventDefault();
123
133
  return;
124
134
  }
135
+ if (!(target instanceof Element)) return;
136
+ const wrapper = drawerContentRef.current?.closest('[data-vaul-detached-wrapper]');
137
+ if (!wrapper) return;
125
138
  // The footer is rendered via vaul's `detachedSiblings` as a sibling of
126
139
  // Drawer.Content inside [data-vaul-detached-wrapper], so Radix treats
127
140
  // clicks on it as "outside" the content. Don't dismiss for clicks that
128
141
  // 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
- }
142
+ if (wrapper.contains(target)) {
143
+ e.preventDefault();
144
+ return;
134
145
  }
146
+ // Every sheet runs Radix non-modal and portals its overlay + wrapper as
147
+ // siblings into the shared container, so a press on a sheet stacked above
148
+ // this one (its content or its dim overlay) is also delivered here as
149
+ // "outside". Sheets present in document order, so anything after our
150
+ // wrapper belongs to a descendant. Radix may dispatch on the deferred
151
+ // click (touch), after a dismissing descendant has left the DOM — treat
152
+ // disconnected as above too.
153
+ /* eslint-disable no-bitwise */
154
+ const above = Node.DOCUMENT_POSITION_FOLLOWING | Node.DOCUMENT_POSITION_DISCONNECTED;
155
+ if (wrapper.compareDocumentPosition(target) & above) {
156
+ e.preventDefault();
157
+ }
158
+ /* eslint-enable no-bitwise */
135
159
  };
136
160
  const dismissAboveRef = useRef(async () => {});
161
+ const dismissDescendantsRef = useRef(() => {});
137
162
  const methods = useMemo(() => ({
138
163
  present: async (index = 0) => {
139
164
  const detent = validDetentsRef.current[index];
140
165
  if (detent === undefined) {
141
166
  throw new Error(`TrueSheet: present index (${index}) is out of bounds. detents array has ${validDetentsRef.current.length} item(s)`);
142
167
  }
168
+ if (!isOpenRef.current) {
169
+ isPresentingRef.current = true;
170
+ setDisplayedBackgroundIndex(index);
171
+ }
143
172
  setActiveSnapPoint(detent);
144
173
  setIsOpen(true);
145
174
  },
@@ -185,17 +214,40 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
185
214
  const [peekContentHeight, setPeekContentHeight] = useState(0);
186
215
  const contentRef = useRef(null);
187
216
  const peekElRef = useRef(null);
217
+ const measurePeek = useCallback(() => {
218
+ const peekEl = getDOMElement(peekElRef.current);
219
+ const contentEl = getDOMElement(contentRef.current);
220
+ if (peekEl && contentEl) {
221
+ setPeekContentHeight(measurePeekContentHeight(peekEl, contentEl));
222
+ }
223
+ }, []);
224
+
225
+ // Last-attached wins — during a screen swap the incoming peek mounts before
226
+ // the outgoing one unmounts, so the outgoing detach must not clear it.
188
227
  const peekContext = useMemo(() => ({
189
228
  contentRef,
190
- peekRef: peekElRef,
191
- setPeekContentHeight
192
- }), []);
229
+ attachPeek: view => {
230
+ peekElRef.current = view;
231
+ measurePeek();
232
+ },
233
+ detachPeek: view => {
234
+ if (peekElRef.current !== view) return;
235
+ peekElRef.current = null;
236
+ setPeekContentHeight(0);
237
+ },
238
+ measurePeek: view => {
239
+ if (peekElRef.current === view) measurePeek();
240
+ }
241
+ }), [measurePeek]);
242
+
243
+ // The peek's own onLayout only fires when *it* resizes. Content added above
244
+ // it moves it without resizing it, so re-measure its offset whenever the
245
+ // content view's layout changes.
246
+ const handleContentLayout = measurePeek;
193
247
 
194
248
  // An absolute (floating) header overlaps the content, so it contributes no
195
249
  // height to the 'auto' detent measurement.
196
250
  const absoluteHeader = headerOptions?.position === 'absolute';
197
- const absoluteHeaderRef = useRef(absoluteHeader);
198
- absoluteHeaderRef.current = absoluteHeader;
199
251
  const resolvedHeaderStyle = absoluteHeader ? [absoluteHeaderStyle, headerStyle] : headerStyle;
200
252
 
201
253
  // Same for an absolute (floating) footer — rendered via vaul's
@@ -222,115 +274,54 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
222
274
  // A relative footer is laid out below the content, so it's pushed off-screen
223
275
  // at the peek detent and contributes no height.
224
276
  const peekHeight = (header ? headerHeight : 0) + (footer && absoluteFooter ? footerHeight : 0) + peekContentHeight || DEFAULT_PEEK_HEIGHT;
225
-
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
- }, []);
277
+ const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
278
+ const naturalHeightRef = useRef(0);
279
+ const insetBehavior = scrollableOptions?.contentInsetAdjustment ?? 'automatic';
280
+ const footerInsetAdjustment = Boolean(footer) && absoluteFooter && (insetBehavior === 'automatic' || insetBehavior === 'footer');
281
+ const measureContent = validDetents.includes('auto') || isFormSheet;
251
282
  useEffect(() => {
252
- if (!isOpen || !hasAutoDetent) return undefined;
283
+ if (!isOpen) return undefined;
253
284
  let canceled = false;
254
285
  let rafId = 0;
255
- let mutationObserver = null;
256
- let resizeObserver = null;
286
+ let observer;
257
287
  const attach = () => {
258
288
  if (canceled) return;
259
- const drawerEl = drawerContentRef.current;
260
- if (!drawerEl || !drawerEl.isConnected) {
289
+ const layoutEl = sizedLayoutRef.current;
290
+ const drawer = drawerContentRef.current;
291
+ if (!drawer || !layoutEl?.isConnected) {
261
292
  // Radix Presence defers the portal mount; poll until the drawer is live.
262
293
  rafId = window.requestAnimationFrame(attach);
263
294
  return;
264
295
  }
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;
296
+ observer = observeSheetLayout({
297
+ drawer,
298
+ layout: layoutEl,
299
+ getContent: () => getDOMElement(contentRef.current),
300
+ getHeader: () => getDOMElement(headerElRef.current),
301
+ getFooter: () => getDOMElement(footerElRef.current),
302
+ getScrollable: () => getScrollableElement(scrollableRef?.current),
303
+ absoluteHeader,
304
+ absoluteFooter,
305
+ footerInsetAdjustment,
306
+ measureContent,
307
+ onHeightChange: height => {
308
+ naturalHeightRef.current = height;
309
+ setMeasuredContentHeight(height);
284
310
  }
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
311
  });
313
312
  };
314
313
  rafId = window.requestAnimationFrame(attach);
315
314
  return () => {
316
315
  canceled = true;
317
316
  window.cancelAnimationFrame(rafId);
318
- mutationObserver?.disconnect();
319
- resizeObserver?.disconnect();
317
+ observer?.disconnect();
320
318
  };
321
- }, [isOpen, hasAutoDetent, measureNaturalHeight]);
319
+ }, [isOpen, absoluteHeader, absoluteFooter, footerInsetAdjustment, measureContent, scrollableRef]);
322
320
 
323
321
  // Below the last detent a vertical touch pan moves the sheet, not the
324
322
  // content — `[data-vaul-scroll-locked]` disables vertical touch panning on
325
323
  // the scroll container and everything inside it (see vaul/style.css).
326
324
  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
325
  const effectiveMaxContentHeight = useMemo(() => {
335
326
  if (maxContentHeight !== undefined) return maxContentHeight;
336
327
  if (!isFormSheet) return undefined;
@@ -393,16 +384,14 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
393
384
  effectiveDetachedOffset,
394
385
  effectiveMaxContentHeight,
395
386
  peekHeight,
396
- peekContentHeight,
397
- measuredContentHeight
387
+ peekContentHeight
398
388
  });
399
389
  geometryInputsRef.current = {
400
390
  effectiveDetached,
401
391
  effectiveDetachedOffset,
402
392
  effectiveMaxContentHeight,
403
393
  peekHeight,
404
- peekContentHeight,
405
- measuredContentHeight
394
+ peekContentHeight
406
395
  };
407
396
  const computeDetentGeometry = useCallback(() => {
408
397
  const inputs = geometryInputsRef.current;
@@ -410,12 +399,9 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
410
399
  const effectiveH = inputs.effectiveDetached ? windowH - inputs.effectiveDetachedOffset : windowH;
411
400
  // Matches vaul's height ceiling: min(effectiveH, maxContentHeight).
412
401
  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;
402
+ // Reuse the intrinsic measurement during position events so animation
403
+ // frames don't clone the content tree again.
404
+ const measuredHeight = naturalHeightRef.current;
419
405
  const autoHeight = Math.min(measuredHeight > 0 ? measuredHeight : effectiveH / 2, ceiling);
420
406
 
421
407
  // 'peek' is measured live from the DOM (offsetHeight is layout-based, so
@@ -446,7 +432,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
446
432
  positions,
447
433
  values
448
434
  };
449
- }, [measureNaturalHeight]);
435
+ }, []);
450
436
 
451
437
  // Detent info for lifecycle events. Position/detent come from the active
452
438
  // detent's target geometry — not the live DOM rect — so willPresent (drawer
@@ -533,50 +519,20 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
533
519
  detent: values[count - 1]
534
520
  };
535
521
  }, [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
522
  const sizedLayoutRef = useRef(null);
543
523
  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
524
  const handlePositionChange = useCallback(position => {
573
- if (isDraggingRef.current || isSettlingAfterDragRef.current) {
574
- updateSizedLayoutHeight(position);
575
- }
576
525
  const {
577
526
  index,
578
527
  detent
579
528
  } = interpolateFromPosition(position);
529
+ if (isOpenRef.current && !isPresentingRef.current) {
530
+ const {
531
+ effectiveH,
532
+ positions
533
+ } = computeDetentGeometry();
534
+ setDisplayedBackgroundIndex(current => getDetentBackgroundIndex(effectiveH - position, positions.map(top => effectiveH - top), current));
535
+ }
580
536
  onPositionChangeRef.current?.({
581
537
  nativeEvent: {
582
538
  index,
@@ -585,7 +541,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
585
541
  realtime: true
586
542
  }
587
543
  });
588
- }, [interpolateFromPosition, updateSizedLayoutHeight]);
544
+ }, [interpolateFromPosition, computeDetentGeometry]);
589
545
 
590
546
  // Fire onMount once after first render. React-mount is the earliest point
591
547
  // the component is ready for imperative calls, matching the native
@@ -611,6 +567,9 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
611
567
  if (isOpen === wasOpen) return undefined;
612
568
  const present = isOpen;
613
569
  if (!present) {
570
+ // Mirror native: dismissing a sheet takes every sheet stacked above it
571
+ // (iOS dismisses from the presenter; Android runs dismissStack first).
572
+ dismissDescendantsRef.current();
614
573
  // Pair willBlur with willDismiss on dismiss — mirrors native iOS
615
574
  // emitWillDismissEvents (blur fires before dismiss). willPresent is
616
575
  // deferred to `start()` below (needs the mounted drawer's geometry).
@@ -623,6 +582,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
623
582
  }
624
583
  const fireDone = () => {
625
584
  if (present) {
585
+ isPresentingRef.current = false;
626
586
  onDidPresentRef.current?.({
627
587
  nativeEvent: computeDetentInfo()
628
588
  });
@@ -728,12 +688,9 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
728
688
  nativeEvent: computeDetentInfo(true)
729
689
  });
730
690
  }, [computeDetentInfo]);
731
- const handleRelease = useCallback((_event, open) => {
691
+ const handleRelease = useCallback(() => {
732
692
  if (!isDraggingRef.current) return;
733
693
  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
694
  onDragEndRef.current?.({
738
695
  nativeEvent: computeDetentInfo(true)
739
696
  });
@@ -744,10 +701,18 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
744
701
  descendants
745
702
  } = useSheetStack(methodsRef, drawerContentRef, isOpen, isFormSheet);
746
703
  dismissAboveRef.current = dismissAbove;
704
+ // Captured per render: by the time the dismiss effect runs, this sheet has
705
+ // already been popped from the stack, so `dismissAbove` can't find it.
706
+ dismissDescendantsRef.current = () => {
707
+ for (let i = descendants.length - 1; i >= 0; i--) {
708
+ descendants[i].ref.current?.dismiss();
709
+ }
710
+ };
747
711
 
748
712
  // Mirror Android: translate this sheet down to match the deepest descendant's
749
713
  // top so the whole stack visually aligns. Cascades because every ancestor
750
714
  // re-runs whenever the stack (and thus its descendants) changes.
715
+ const wasCascadedRef = useRef(false);
751
716
  useEffect(() => {
752
717
  const parent = drawerContentRef.current;
753
718
  if (!parent) return;
@@ -758,7 +723,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
758
723
  // out — nothing to align with anymore.
759
724
  if (!isOpen) return;
760
725
  const parentWrapper = parent.closest('[data-vaul-detached-wrapper]');
761
- const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
726
+ const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')}), --snap-point-height ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
762
727
  const wrapperTransition = `clip-path ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
763
728
  const CLIP_NONE = 'inset(0px round 0px)';
764
729
 
@@ -781,22 +746,29 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
781
746
  parentWrapper.style.clipPath = next;
782
747
  };
783
748
  if (descendants.length === 0) {
749
+ if (!wasCascadedRef.current) return;
750
+ wasCascadedRef.current = false;
784
751
  parent.style.transition = transition;
785
752
  parent.style.transform = '';
786
753
  setClip(CLIP_NONE);
787
754
  return;
788
755
  }
756
+ wasCascadedRef.current = true;
789
757
 
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.
758
+ // Track the immediate child's *actual* top: its cascade transform when it
759
+ // was itself pushed behind a grandchild, else its own snap point. Mirrors
760
+ // Android's addTranslation propagating extra translation up the stack —
761
+ // a grandchild opening pushes this sheet only as far as our child moved.
762
+ const readTranslateY = el => {
763
+ const match = el.style.transform.match(/translate3d\([^,]*,\s*(-?\d*\.?\d+)px/);
764
+ return match ? Number.parseFloat(match[1]) : null;
765
+ };
794
766
  const computeTargetY = () => {
795
767
  const parentSnap = Number.parseFloat(parent.style.getPropertyValue('--snap-point-height')) || 0;
796
768
  const node = descendants[0]?.nodeRef.current;
797
769
  if (!node) return parentSnap;
798
- const childSnap = Number.parseFloat(node.style.getPropertyValue('--snap-point-height')) || 0;
799
- return Math.max(parentSnap, childSnap);
770
+ const childTop = readTranslateY(node) ?? (Number.parseFloat(node.style.getPropertyValue('--snap-point-height')) || 0);
771
+ return Math.max(parentSnap, childTop);
800
772
  };
801
773
 
802
774
  // When a form-sheet parent has a form-sheet descendant, clip the parent to
@@ -830,18 +802,26 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
830
802
  };
831
803
  const apply = () => {
832
804
  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) {
805
+ // Mirror iPad/Android (`isExpanded`): a parent at full height (detent 1)
806
+ // stays put and the child simply overlays it. Full height is the sheet's
807
+ // own ceiling — the viewport for a page sheet, the capped content-fit
808
+ // height for a form sheet — so compare against where detent 1 lands.
809
+ const parentSnap = Number.parseFloat(parent.style.getPropertyValue('--snap-point-height')) || 0;
810
+ const {
811
+ effectiveH,
812
+ ceiling
813
+ } = computeDetentGeometry();
814
+ if (parentSnap <= effectiveH - ceiling + 0.5) {
838
815
  parent.style.transition = transition;
839
816
  parent.style.transform = '';
840
817
  return;
841
818
  }
819
+ // Mirror Android: stay put while the child is being dragged (its live
820
+ // transform would drag us along during drag-to-dismiss). The release
821
+ // snap rewrites the child's style and re-applies.
822
+ if (descendants[0]?.nodeRef.current?.classList.contains(DRAG_CLASS)) return;
842
823
  const targetY = computeTargetY();
843
- const match = parent.style.transform.match(/translate3d\([^,]*,\s*(-?\d*\.?\d+)px/);
844
- const currentY = match ? Number.parseFloat(match[1]) : 0;
824
+ const currentY = readTranslateY(parent) ?? 0;
845
825
  if (Math.abs(currentY - targetY) < 0.5) return;
846
826
  parent.style.transition = transition;
847
827
  parent.style.transform = `translate3d(0, ${targetY}px, 0)`;
@@ -855,11 +835,17 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
855
835
  attributes: true,
856
836
  attributeFilter: ['style']
857
837
  });
838
+ // The child's own cascade lands in a later frame — follow it when it moves.
839
+ const childNode = descendants[0]?.nodeRef.current;
840
+ if (childNode) observer.observe(childNode, {
841
+ attributes: true,
842
+ attributeFilter: ['style']
843
+ });
858
844
  return () => {
859
845
  cancelAnimationFrame(raf);
860
846
  observer.disconnect();
861
847
  };
862
- }, [descendants, activeSnapPoint, cornerRadius, isFormSheet, isOpen]);
848
+ }, [descendants, activeSnapPoint, cornerRadius, isFormSheet, isOpen, computeDetentGeometry]);
863
849
 
864
850
  // Focus/blur events fire when a descendant sheet appears on top of this one
865
851
  // (blur) or when all descendants are dismissed (focus). will-events fire
@@ -870,12 +856,19 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
870
856
  const onDidBlurRef = useRef(onDidBlur);
871
857
  const onWillFocusRef = useRef(onWillFocus);
872
858
  const onDidFocusRef = useRef(onDidFocus);
859
+ const onDismissAttemptRef = useRef(onDismissAttempt);
873
860
  useEffect(() => {
874
861
  onWillBlurRef.current = onWillBlur;
875
862
  onDidBlurRef.current = onDidBlur;
876
863
  onWillFocusRef.current = onWillFocus;
877
864
  onDidFocusRef.current = onDidFocus;
865
+ onDismissAttemptRef.current = onDismissAttempt;
878
866
  });
867
+ const handleDismissAttempt = useCallback(() => {
868
+ onDismissAttemptRef.current?.({
869
+ nativeEvent: null
870
+ });
871
+ }, []);
879
872
  const prevDescendantCountRef = useRef(0);
880
873
  useEffect(() => {
881
874
  const prevCount = prevDescendantCountRef.current;
@@ -924,11 +917,6 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
924
917
  window.cancelAnimationFrame(rafId);
925
918
  };
926
919
  }, [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
920
  const effectiveCornerRadius = cornerRadius ?? DEFAULT_CORNER_RADIUS;
933
921
 
934
922
  // Shadow cast upward from the sheet's top edge toward the background. Matches
@@ -942,6 +930,13 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
942
930
  // wrapper edges). drop-shadow on the wrapper follows the post-clip silhouette
943
931
  // and isn't clipped by the wrapper itself.
944
932
  const dropShadow = elevation > 0 ? `drop-shadow(0 ${-elevation}px ${elevation * 3}px rgba(0, 0, 0, 0.15))` : undefined;
933
+ const backgroundLayerStyle = {
934
+ position: 'absolute',
935
+ inset: 0,
936
+ pointerEvents: 'none',
937
+ backgroundColor: backgroundColor,
938
+ transition: isPresentingRef.current ? 'none' : 'background-color 200ms'
939
+ };
945
940
  const mergedContentStyle = useMemo(() => ({
946
941
  position: 'fixed',
947
942
  top: 0,
@@ -952,7 +947,6 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
952
947
  flexDirection: 'column',
953
948
  borderTopLeftRadius: effectiveCornerRadius,
954
949
  borderTopRightRadius: effectiveCornerRadius,
955
- backgroundColor: backgroundColor,
956
950
  // Clip children to the rounded top so headers/content with their own
957
951
  // background don't bleed past the corners. `clip` (not `hidden`): a
958
952
  // hidden box is still a scroll container, so browser focus-reveal (e.g.
@@ -962,8 +956,11 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
962
956
  // Lift content above iOS home indicator / bottom safe area when enabled.
963
957
  // A relative footer owns the inset instead (see resolvedFooterStyle); an
964
958
  // 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]);
959
+ // A plugged scrollable keeps the lift too (contentInsetAdjustment
960
+ // 'automatic' | 'safe-area') — 'footer' | 'never' scroll edge-to-edge
961
+ // behind the indicator with user-applied content padding.
962
+ paddingBottom: insetAdjustment === 'automatic' && !(footerOwnsInset && !absoluteFooter) && (!scrollableRef || insetBehavior === 'automatic' || insetBehavior === 'safe-area') ? 'env(safe-area-inset-bottom, 0px)' : 0
963
+ }), [effectiveCornerRadius, insetAdjustment, footerOwnsInset, absoluteFooter, scrollableRef, insetBehavior]);
967
964
  const defaultGrabberColor = colorScheme === 'dark' ? DEFAULT_GRABBER_COLOR_DARK : DEFAULT_GRABBER_COLOR_LIGHT;
968
965
  const grabberHeight = grabberOptions?.height ?? DEFAULT_GRABBER_HEIGHT;
969
966
 
@@ -994,7 +991,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
994
991
  // `maxContentWidth` is ignored ('form' is absolute).
995
992
  // - presentation='page' → `maxContentWidth` (any viewport) or
996
993
  // DEFAULT_MAX_WIDTH (tablet/landscape readability cap).
997
- // Detached without a width constraint applies anchorOffset on both edges so
994
+ // Detached without a width constraint applies placementOffset on both edges so
998
995
  // the floating card breathes from the viewport sides.
999
996
  const wrapperStyle = useMemo(() => {
1000
997
  // Mobile portrait ignores width sizing entirely (matches iOS/Android:
@@ -1006,23 +1003,23 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
1006
1003
  const next = {};
1007
1004
  if (dropShadow) next.filter = dropShadow;
1008
1005
  if (!needsMargins) return next;
1009
- let marginLeft;
1010
- let marginRight;
1006
+ let marginInlineStart;
1007
+ let marginInlineEnd;
1011
1008
  if (isFormSheet) {
1012
- marginLeft = 'auto';
1013
- marginRight = 'auto';
1009
+ marginInlineStart = 'auto';
1010
+ marginInlineEnd = 'auto';
1014
1011
  } else if (maxWidth == null) {
1015
- marginLeft = anchorOffset;
1016
- marginRight = anchorOffset;
1012
+ marginInlineStart = placementOffset;
1013
+ marginInlineEnd = placementOffset;
1017
1014
  } else {
1018
- marginLeft = anchor === 'left' ? anchorOffset : 'auto';
1019
- marginRight = anchor === 'right' ? anchorOffset : 'auto';
1015
+ marginInlineStart = placement === 'leading' ? placementOffset : 'auto';
1016
+ marginInlineEnd = placement === 'trailing' ? placementOffset : 'auto';
1020
1017
  }
1021
1018
  if (maxWidth != null) next.maxWidth = maxWidth;
1022
- next.marginLeft = marginLeft;
1023
- next.marginRight = marginRight;
1019
+ next.marginInlineStart = marginInlineStart;
1020
+ next.marginInlineEnd = marginInlineEnd;
1024
1021
  return next;
1025
- }, [isLandscapeOrTablet, isFormSheet, maxContentWidth, presentation, anchor, anchorOffset, effectiveDetached, dropShadow]);
1022
+ }, [isLandscapeOrTablet, isFormSheet, maxContentWidth, presentation, placement, placementOffset, effectiveDetached, dropShadow]);
1026
1023
 
1027
1024
  // Absolute-position the grabber so it overlays the content top-edge
1028
1025
  // instead of consuming flow height — mirrors native iOS/Android, where
@@ -1052,6 +1049,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
1052
1049
  onDrag: handleDrag,
1053
1050
  onRelease: handleRelease,
1054
1051
  dismissible: dismissible,
1052
+ onDismissAttempt: handleDismissAttempt,
1055
1053
  draggable: draggable,
1056
1054
  repositionInputs: false,
1057
1055
  modal: dimmed,
@@ -1063,11 +1061,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
1063
1061
  peekHeight: peekHeight,
1064
1062
  initialAnimated: initialDetentAnimated,
1065
1063
  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,
1064
+ contentHeight: measuredContentHeight,
1071
1065
  activeSnapPoint: activeSnapPoint,
1072
1066
  setActiveSnapPoint: handleSetActiveSnapPoint,
1073
1067
  ...snapPointsProps,
@@ -1088,31 +1082,30 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
1088
1082
  children: /*#__PURE__*/isValidElement(footer) ? footer : /*#__PURE__*/createElement(footer)
1089
1083
  })
1090
1084
  }) : undefined,
1091
- children: [/*#__PURE__*/_jsx(Drawer.Title, {
1085
+ children: [/*#__PURE__*/_jsx("div", {
1086
+ "aria-hidden": true,
1087
+ style: backgroundLayerStyle
1088
+ }), /*#__PURE__*/_jsx(Drawer.Title, {
1092
1089
  style: visuallyHiddenStyle,
1093
1090
  children: accessibilityOptions?.paneTitle ?? 'Sheet'
1094
1091
  }), grabber && /*#__PURE__*/_jsx(Drawer.Handle, {
1095
1092
  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", {
1093
+ }), /*#__PURE__*/_jsxs("div", {
1105
1094
  ref: sizedLayoutRef,
1106
1095
  style: sizedLayoutStyle,
1107
1096
  "data-vaul-scroll-locked": isScrollLocked ? '' : undefined,
1108
- children: [header && /*#__PURE__*/_jsx(View, {
1097
+ children: [/*#__PURE__*/_jsx(TrueSheetBackground, {
1098
+ background: background,
1099
+ backgroundStyle: backgroundStyle
1100
+ }), header && /*#__PURE__*/_jsx(View, {
1109
1101
  ref: headerElRef,
1110
1102
  style: resolvedHeaderStyle,
1111
1103
  onLayout: handleHeaderLayout,
1112
1104
  children: /*#__PURE__*/isValidElement(header) ? header : /*#__PURE__*/createElement(header)
1113
1105
  }), /*#__PURE__*/_jsx(View, {
1114
1106
  ref: contentRef,
1115
- style: hasAutoDetent && hasBoundedScrollable ? [contentFillStyle, style] : style,
1107
+ style: [contentStyle, style],
1108
+ onLayout: handleContentLayout,
1116
1109
  children: children
1117
1110
  }), footer && !absoluteFooter && /*#__PURE__*/_jsx(View, {
1118
1111
  ref: footerElRef,
@@ -1120,26 +1113,6 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
1120
1113
  onLayout: handleFooterLayout,
1121
1114
  children: /*#__PURE__*/isValidElement(footer) ? footer : /*#__PURE__*/createElement(footer)
1122
1115
  })]
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
1116
  })]
1144
1117
  })]
1145
1118
  })
@@ -1173,10 +1146,12 @@ const sizedLayoutStyle = {
1173
1146
  right: 0,
1174
1147
  height: SIZED_LAYOUT_HEIGHT,
1175
1148
  display: 'flex',
1176
- flexDirection: 'column'
1149
+ flexDirection: 'column',
1150
+ boxSizing: 'border-box',
1151
+ paddingBottom: 'inherit'
1177
1152
  };
1178
- const contentFillStyle = {
1179
- flex: 1,
1153
+ const contentStyle = {
1154
+ flexShrink: 1,
1180
1155
  minHeight: 0
1181
1156
  };
1182
1157
  const visuallyHiddenStyle = {