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

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 (73) hide show
  1. package/README.md +6 -5
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +23 -2
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +116 -26
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +7 -0
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +48 -0
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterViewManager.kt +7 -0
  7. package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +45 -0
  8. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +28 -12
  9. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +99 -34
  10. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +9 -5
  11. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +12 -3
  12. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +33 -4
  13. package/android/src/main/jni/TrueSheetSpec.h +2 -0
  14. package/android/src/main/jni/TrueSheetStateUpdater.cpp +96 -0
  15. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +24 -0
  16. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +43 -0
  17. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +28 -0
  18. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +11 -0
  19. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +37 -0
  20. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h +24 -0
  21. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +56 -0
  22. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +28 -0
  23. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +11 -0
  24. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +41 -0
  25. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.cpp +18 -0
  26. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.h +16 -0
  27. package/ios/TrueSheetContainerView.h +12 -10
  28. package/ios/TrueSheetContainerView.mm +11 -29
  29. package/ios/TrueSheetContentView.h +22 -7
  30. package/ios/TrueSheetContentView.mm +143 -41
  31. package/ios/TrueSheetFooterView.h +12 -2
  32. package/ios/TrueSheetFooterView.mm +74 -71
  33. package/ios/TrueSheetHeaderView.mm +0 -1
  34. package/ios/TrueSheetView.mm +60 -27
  35. package/ios/TrueSheetViewController.h +9 -0
  36. package/ios/TrueSheetViewController.mm +288 -122
  37. package/ios/core/TrueSheetDetentCalculator.h +9 -0
  38. package/ios/core/TrueSheetDetentCalculator.mm +13 -4
  39. package/ios/tvos/TrueSheetStubs.mm +2 -0
  40. package/lib/module/TrueSheet.js +32 -13
  41. package/lib/module/TrueSheet.js.map +1 -1
  42. package/lib/module/TrueSheet.web.js +262 -38
  43. package/lib/module/TrueSheet.web.js.map +1 -1
  44. package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +5 -1
  45. package/lib/module/fabric/TrueSheetFooterViewNativeComponent.ts +10 -2
  46. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +8 -1
  47. package/lib/module/web/vaul/index.js +17 -6
  48. package/lib/module/web/vaul/index.js.map +1 -1
  49. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  50. package/lib/typescript/src/TrueSheet.types.d.ts +35 -11
  51. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  52. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  53. package/lib/typescript/src/fabric/TrueSheetContentViewNativeComponent.d.ts.map +1 -1
  54. package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts +2 -0
  55. package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts.map +1 -1
  56. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +5 -1
  57. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  58. package/lib/typescript/src/navigation/types.d.ts +1 -1
  59. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  60. package/lib/typescript/src/web/vaul/index.d.ts +8 -1
  61. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
  62. package/package.json +10 -10
  63. package/react-native.config.js +5 -1
  64. package/src/TrueSheet.tsx +45 -14
  65. package/src/TrueSheet.types.ts +38 -12
  66. package/src/TrueSheet.web.tsx +314 -44
  67. package/src/fabric/TrueSheetContentViewNativeComponent.ts +5 -1
  68. package/src/fabric/TrueSheetFooterViewNativeComponent.ts +10 -2
  69. package/src/fabric/TrueSheetViewNativeComponent.ts +8 -1
  70. package/src/navigation/types.ts +3 -1
  71. package/src/web/vaul/index.tsx +23 -5
  72. package/ios/utils/LayoutUtil.h +0 -77
  73. package/ios/utils/LayoutUtil.mm +0 -106
@@ -2,13 +2,26 @@
2
2
 
3
3
  /// <reference lib="dom" />
4
4
  import { createElement, forwardRef, isValidElement, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
5
- import { useColorScheme, useWindowDimensions, View } from 'react-native';
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
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";
9
9
  import { Drawer } from "./web/vaul/index.js";
10
10
  import { DEFAULT_PEEK_HEIGHT, TRANSITIONS } from "./web/vaul/constants.js";
11
+
12
+ // First vertically scrollable descendant — web mirror of native's
13
+ // findScrollView (RN-web ScrollView/FlatList render a node with
14
+ // `overflow-y: auto`; its first child is the content container).
11
15
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
16
+ const findVerticalScroller = root => {
17
+ for (const el of root.querySelectorAll('*')) {
18
+ const {
19
+ overflowY
20
+ } = window.getComputedStyle(el);
21
+ if (overflowY === 'auto' || overflowY === 'scroll') return el;
22
+ }
23
+ return null;
24
+ };
12
25
  const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
13
26
  const {
14
27
  children,
@@ -31,9 +44,10 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
31
44
  initialDetentIndex = -1,
32
45
  header,
33
46
  headerStyle,
47
+ headerOptions,
34
48
  footer,
35
49
  footerStyle,
36
- scrollable = false,
50
+ footerOptions,
37
51
  presentation = 'page',
38
52
  detached = false,
39
53
  detachedOffset = DEFAULT_DETACHED_OFFSET,
@@ -88,6 +102,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
88
102
  }, [validDetents]);
89
103
  const validDetentsRef = useRef(validDetents);
90
104
  validDetentsRef.current = validDetents;
105
+ const hasAutoDetent = validDetents.includes('auto');
91
106
  const handleSetActiveSnapPoint = useCallback(snapPoint => {
92
107
  setActiveSnapPoint(snapPoint == null ? null : typeof snapPoint === 'number' || snapPoint === 'auto' || snapPoint === 'peek' ? snapPoint : null);
93
108
  }, []);
@@ -174,7 +189,134 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
174
189
  peekRef: peekElRef,
175
190
  setPeekContentHeight
176
191
  }), []);
177
- const peekHeight = (header ? headerHeight : 0) + (footer ? footerHeight : 0) + peekContentHeight || DEFAULT_PEEK_HEIGHT;
192
+
193
+ // An absolute (floating) header overlaps the content, so it contributes no
194
+ // height to the 'auto' detent measurement.
195
+ const absoluteHeader = headerOptions?.position === 'absolute';
196
+ const absoluteHeaderRef = useRef(absoluteHeader);
197
+ absoluteHeaderRef.current = absoluteHeader;
198
+ const resolvedHeaderStyle = absoluteHeader ? [absoluteHeaderStyle, headerStyle] : headerStyle;
199
+
200
+ // Same for an absolute (floating) footer — rendered via vaul's
201
+ // `detachedSiblings` instead of in the content flow.
202
+ const absoluteFooter = footerOptions?.position === 'absolute';
203
+ const absoluteFooterRef = useRef(absoluteFooter);
204
+ absoluteFooterRef.current = absoluteFooter;
205
+
206
+ // The footer owns the sheet's bottom edge, so it absorbs the bottom
207
+ // safe-area inset as padding (on top of its own) — mirrors native, where the
208
+ // footer shadow node pads the inset so its background fills it.
209
+ const footerOwnsInset = Boolean(footer) && insetAdjustment === 'automatic';
210
+ const resolvedFooterStyle = useMemo(() => {
211
+ if (!footerOwnsInset) return footerStyle;
212
+ const flat = StyleSheet.flatten(footerStyle);
213
+ const base = flat?.paddingBottom ?? flat?.paddingVertical ?? flat?.padding;
214
+ const baseCss = typeof base === 'number' ? `${base}px` : typeof base === 'string' ? base : '0px';
215
+ return [footerStyle, {
216
+ paddingBottom: `calc(${baseCss} + env(safe-area-inset-bottom, 0px))`
217
+ }];
218
+ }, [footerOwnsInset, footerStyle]);
219
+
220
+ // A relative footer is laid out below the content, so it's pushed off-screen
221
+ // at the peek detent and contributes no height.
222
+ const peekHeight = (header ? headerHeight : 0) + (footer && absoluteFooter ? footerHeight : 0) + peekContentHeight || DEFAULT_PEEK_HEIGHT;
223
+
224
+ // Web mirror of native's scrollable handling for 'auto' detents: a plugged
225
+ // scrollable keeps the sized (bounded) layout so its viewport is capped to
226
+ // the visible sheet and can scroll, while the 'auto' height is measured with
227
+ // the viewport replaced by the scrollable's content size — mirrors native
228
+ // `naturalHeight`.
229
+ const [hasBoundedScrollable, setHasBoundedScrollable] = useState(false);
230
+ const [scrollableAutoHeight, setScrollableAutoHeight] = useState(0);
231
+ const pinnedScrollerRef = useRef(null);
232
+
233
+ // Natural content height (header + content + footer, with a pinned
234
+ // scrollable's viewport replaced by its content size) — the height the
235
+ // content wants regardless of the sheet's bounds.
236
+ const measureNaturalHeight = useCallback(() => {
237
+ const contentEl = contentRef.current;
238
+ if (!contentEl || !contentEl.isConnected) return 0;
239
+ const headerEl = absoluteHeaderRef.current ? null : headerElRef.current;
240
+ const footerEl = absoluteFooterRef.current ? null : footerElRef.current;
241
+ let height = (headerEl?.offsetHeight ?? 0) + (footerEl?.offsetHeight ?? 0) + contentEl.offsetHeight;
242
+ const scroller = pinnedScrollerRef.current;
243
+ const scrollContent = scroller?.firstElementChild;
244
+ if (scroller?.isConnected && scrollContent instanceof HTMLElement) {
245
+ height += scrollContent.offsetHeight - scroller.clientHeight;
246
+ }
247
+ return Math.max(0, height);
248
+ }, []);
249
+ useEffect(() => {
250
+ if (!isOpen || !hasAutoDetent) return undefined;
251
+ let canceled = false;
252
+ let rafId = 0;
253
+ let mutationObserver = null;
254
+ let resizeObserver = null;
255
+ const attach = () => {
256
+ if (canceled) return;
257
+ const drawerEl = drawerContentRef.current;
258
+ if (!drawerEl || !drawerEl.isConnected) {
259
+ // Radix Presence defers the portal mount; poll until the drawer is live.
260
+ rafId = window.requestAnimationFrame(attach);
261
+ return;
262
+ }
263
+ const measure = () => setScrollableAutoHeight(measureNaturalHeight());
264
+ let observed = null;
265
+
266
+ // (Re)pin the first vertical scrollable and observe the nodes whose size
267
+ // feeds the natural height — content growth inside a bounded scroller is
268
+ // invisible to the sheet's own layout (mirrors native's contentSize
269
+ // observation). Resolves the content node live: the layout-branch swap
270
+ // (natural flow ↔ sized) remounts it.
271
+ const observe = () => {
272
+ const contentEl = contentRef.current;
273
+ const headerEl = headerElRef.current;
274
+
275
+ // Skip mutations that don't change the observed topology — e.g. a
276
+ // virtualized list mounting rows inside the pinned scroller, which
277
+ // would otherwise trigger a full-subtree rescan (findVerticalScroller
278
+ // resolves computed styles) on every scroll frame. Size changes are
279
+ // already covered by the ResizeObserver.
280
+ if (observed && observed.contentEl === contentEl && contentEl?.isConnected && observed.headerEl === headerEl && observed.scroller.isConnected && observed.scrollContent === observed.scroller.firstElementChild) {
281
+ return;
282
+ }
283
+ const scroller = contentEl && contentEl.isConnected ? findVerticalScroller(contentEl) : null;
284
+ pinnedScrollerRef.current = scroller;
285
+ setHasBoundedScrollable(scroller != null);
286
+ observed = scroller ? {
287
+ contentEl,
288
+ headerEl,
289
+ scroller,
290
+ scrollContent: scroller.firstElementChild
291
+ } : null;
292
+ resizeObserver?.disconnect();
293
+ resizeObserver = new ResizeObserver(measure);
294
+ if (contentEl?.isConnected) resizeObserver.observe(contentEl);
295
+ if (headerEl) resizeObserver.observe(headerEl);
296
+ if (scroller) {
297
+ resizeObserver.observe(scroller);
298
+ if (scroller.firstElementChild) resizeObserver.observe(scroller.firstElementChild);
299
+ }
300
+ measure();
301
+ };
302
+ observe();
303
+ // Watch the whole drawer subtree — catches the scrollable mounting/
304
+ // unmounting AND the layout-branch swap, which remounts the content node
305
+ // (a content-scoped observer would go stale after the swap).
306
+ mutationObserver = new MutationObserver(observe);
307
+ mutationObserver.observe(drawerEl, {
308
+ childList: true,
309
+ subtree: true
310
+ });
311
+ };
312
+ rafId = window.requestAnimationFrame(attach);
313
+ return () => {
314
+ canceled = true;
315
+ window.cancelAnimationFrame(rafId);
316
+ mutationObserver?.disconnect();
317
+ resizeObserver?.disconnect();
318
+ };
319
+ }, [isOpen, hasAutoDetent, measureNaturalHeight]);
178
320
 
179
321
  // Below the last detent a vertical touch pan moves the sheet, not the
180
322
  // content — `[data-vaul-scroll-locked]` disables vertical touch panning on
@@ -266,13 +408,12 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
266
408
  const effectiveH = inputs.effectiveDetached ? windowH - inputs.effectiveDetachedOffset : windowH;
267
409
  // Matches vaul's height ceiling: min(effectiveH, maxContentHeight).
268
410
  const ceiling = inputs.effectiveMaxContentHeight !== undefined ? Math.min(effectiveH, inputs.effectiveMaxContentHeight) : effectiveH;
269
- // 'auto' resolves to the auto-size wrapper's height. Read it live — the
411
+ // 'auto' resolves to the content's natural height. Read it live — the
270
412
  // `measuredContentHeight` state lags mount by a frame (vaul's initial
271
413
  // ref-callback measure runs while the portal subtree is still detached,
272
414
  // so it reads 0 until the ResizeObserver fires). Falls back to the state
273
415
  // value, then to vaul's pre-measure fallback (effectiveHeight / 2).
274
- const autoWrapper = drawerContentRef.current?.querySelector('[data-vaul-auto-size-wrapper]');
275
- const measuredHeight = autoWrapper?.offsetHeight || inputs.measuredContentHeight;
416
+ const measuredHeight = measureNaturalHeight() || inputs.measuredContentHeight;
276
417
  const autoHeight = Math.min(measuredHeight > 0 ? measuredHeight : effectiveH / 2, ceiling);
277
418
 
278
419
  // 'peek' is measured live from the DOM (offsetHeight is layout-based, so
@@ -283,7 +424,8 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
283
424
  // Geometry only runs while the drawer is mounted, so the elements are
284
425
  // measurable here; fall back to the state value when they're absent.
285
426
  const headerEl = headerElRef.current;
286
- const footerEl = footerElRef.current;
427
+ // A relative footer is pushed off-screen at the peek detent — excluded
428
+ const footerEl = absoluteFooterRef.current ? footerElRef.current : null;
287
429
  const peekEl = peekElRef.current;
288
430
  const contentEl = contentRef.current;
289
431
  const livePeekContentHeight = peekEl && contentEl ? measurePeekContentHeight(peekEl, contentEl) : inputs.peekContentHeight;
@@ -297,10 +439,12 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
297
439
  }
298
440
  return {
299
441
  windowH,
442
+ effectiveH,
443
+ ceiling,
300
444
  positions,
301
445
  values
302
446
  };
303
- }, []);
447
+ }, [measureNaturalHeight]);
304
448
 
305
449
  // Detent info for lifecycle events. Position/detent come from the active
306
450
  // detent's target geometry — not the live DOM rect — so willPresent (drawer
@@ -387,7 +531,46 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
387
531
  detent: values[count - 1]
388
532
  };
389
533
  }, [computeDetentGeometry]);
534
+
535
+ // Mirror native synchronous layout: while dragging (and through the post-
536
+ // release settle) the sized layout tracks the sheet's visible height in
537
+ // realtime, clamped to the smallest detent — dragging below it slides the
538
+ // sheet out without resizing. Once settled, the inline height is cleared so
539
+ // the CSS calc() owns sizing again (adapts to viewport resizes at rest).
540
+ const sizedLayoutRef = useRef(null);
541
+ const isDraggingRef = useRef(false);
542
+ const isSettlingAfterDragRef = useRef(false);
543
+ const updateSizedLayoutHeight = useCallback(position => {
544
+ const node = sizedLayoutRef.current;
545
+ if (!node) return;
546
+ const {
547
+ effectiveH,
548
+ ceiling,
549
+ positions
550
+ } = computeDetentGeometry();
551
+ if (positions.length === 0) return;
552
+ if (isSettlingAfterDragRef.current) {
553
+ const snap = activeSnapPointRef.current;
554
+ const index = snap != null ? validDetentsRef.current.indexOf(snap) : -1;
555
+ const target = index >= 0 ? positions[index] : null;
556
+ if (target != null && Math.abs(position - target) < 1) {
557
+ isSettlingAfterDragRef.current = false;
558
+ // Restore the calc — height is an inline style, so clearing it would
559
+ // leave the div unsized (React won't re-apply it without a render)
560
+ node.style.height = SIZED_LAYOUT_HEIGHT;
561
+ return;
562
+ }
563
+ }
564
+
565
+ // positions[0] is the smallest detent's top-Y — its height is the clamp floor.
566
+ const minHeight = effectiveH - positions[0];
567
+ const height = Math.min(Math.max(effectiveH - position, minHeight), ceiling);
568
+ node.style.height = `${height}px`;
569
+ }, [computeDetentGeometry]);
390
570
  const handlePositionChange = useCallback(position => {
571
+ if (isDraggingRef.current || isSettlingAfterDragRef.current) {
572
+ updateSizedLayoutHeight(position);
573
+ }
391
574
  const {
392
575
  index,
393
576
  detent
@@ -400,7 +583,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
400
583
  realtime: true
401
584
  }
402
585
  });
403
- }, [interpolateFromPosition]);
586
+ }, [interpolateFromPosition, updateSizedLayoutHeight]);
404
587
 
405
588
  // Fire onMount once after first render. React-mount is the earliest point
406
589
  // the component is ready for imperative calls, matching the native
@@ -532,7 +715,6 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
532
715
 
533
716
  // Vaul's `onDrag` fires once per pointermove while dragging; the first tick
534
717
  // after an idle gap marks the drag boundary, so track it via a ref.
535
- const isDraggingRef = useRef(false);
536
718
  const handleDrag = useCallback(() => {
537
719
  if (!isDraggingRef.current) {
538
720
  isDraggingRef.current = true;
@@ -544,9 +726,12 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
544
726
  nativeEvent: computeDetentInfo(true)
545
727
  });
546
728
  }, [computeDetentInfo]);
547
- const handleRelease = useCallback(() => {
729
+ const handleRelease = useCallback((_event, open) => {
548
730
  if (!isDraggingRef.current) return;
549
731
  isDraggingRef.current = false;
732
+ // Track the sized layout through the settle animation only when the sheet
733
+ // stays open — a dismissal slides out at the min-clamped height.
734
+ isSettlingAfterDragRef.current = open;
550
735
  onDragEndRef.current?.({
551
736
  nativeEvent: computeDetentInfo(true)
552
737
  });
@@ -737,6 +922,11 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
737
922
  window.cancelAnimationFrame(rafId);
738
923
  };
739
924
  }, [isOpen, descendants.length]);
925
+
926
+ // Definite-height flex layout (per-detent sizing) unless content must be
927
+ // measured in natural flow: 'auto' detents (without a plugged scrollable —
928
+ // those measure via `measureNaturalHeight`) and form-sheet content-fit sizing.
929
+ const useSizedLayout = (!hasAutoDetent || hasBoundedScrollable) && !isFormSheet;
740
930
  const effectiveCornerRadius = cornerRadius ?? DEFAULT_CORNER_RADIUS;
741
931
 
742
932
  // Shadow cast upward from the sheet's top edge toward the background. Matches
@@ -762,11 +952,16 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
762
952
  borderTopRightRadius: effectiveCornerRadius,
763
953
  backgroundColor: backgroundColor,
764
954
  // Clip children to the rounded top so headers/content with their own
765
- // background don't bleed past the corners.
766
- overflow: 'hidden',
955
+ // background don't bleed past the corners. `clip` (not `hidden`): a
956
+ // hidden box is still a scroll container, so browser focus-reveal (e.g.
957
+ // tapping a button near the sheet bottom) can scroll-offset the drawer —
958
+ // shifting content and blocking vaul's shouldDrag walk (scrollTop > 0).
959
+ overflow: 'clip',
767
960
  // Lift content above iOS home indicator / bottom safe area when enabled.
768
- paddingBottom: insetAdjustment === 'automatic' ? 'env(safe-area-inset-bottom, 0px)' : 0
769
- }), [backgroundColor, effectiveCornerRadius, insetAdjustment]);
961
+ // A relative footer owns the inset instead (see resolvedFooterStyle); an
962
+ // absolute footer floats over the content, so the content keeps its lift.
963
+ paddingBottom: insetAdjustment === 'automatic' && !(footerOwnsInset && !absoluteFooter) ? 'env(safe-area-inset-bottom, 0px)' : 0
964
+ }), [backgroundColor, effectiveCornerRadius, insetAdjustment, footerOwnsInset, absoluteFooter]);
770
965
  const defaultGrabberColor = colorScheme === 'dark' ? DEFAULT_GRABBER_COLOR_DARK : DEFAULT_GRABBER_COLOR_LIGHT;
771
966
  const grabberHeight = grabberOptions?.height ?? DEFAULT_GRABBER_HEIGHT;
772
967
 
@@ -867,6 +1062,10 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
867
1062
  initialAnimated: initialDetentAnimated,
868
1063
  detachedWrapperStyle: wrapperStyle,
869
1064
  onContentHeightChange: setMeasuredContentHeight,
1065
+ contentHeight:
1066
+ // Bounded scrollable → the auto-size wrapper measures 0 (sized
1067
+ // layout is absolute), so feed vaul the natural height instead.
1068
+ hasAutoDetent && hasBoundedScrollable && scrollableAutoHeight > 0 ? scrollableAutoHeight : undefined,
870
1069
  activeSnapPoint: activeSnapPoint,
871
1070
  setActiveSnapPoint: handleSetActiveSnapPoint,
872
1071
  ...snapPointsProps,
@@ -878,11 +1077,11 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
878
1077
  ref: drawerContentRef,
879
1078
  style: mergedContentStyle,
880
1079
  onPointerDownOutside: handlePointerDownOutside,
881
- detachedSiblings: footer ? /*#__PURE__*/_jsx("div", {
1080
+ detachedSiblings: footer && absoluteFooter ? /*#__PURE__*/_jsx("div", {
882
1081
  style: footerFloatStyle,
883
1082
  children: /*#__PURE__*/_jsx(View, {
884
1083
  ref: footerElRef,
885
- style: footerStyle,
1084
+ style: resolvedFooterStyle,
886
1085
  onLayout: handleFooterLayout,
887
1086
  children: /*#__PURE__*/isValidElement(footer) ? footer : /*#__PURE__*/createElement(footer)
888
1087
  })
@@ -892,39 +1091,52 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
892
1091
  children: accessibilityOptions?.paneTitle ?? 'Sheet'
893
1092
  }), grabber && /*#__PURE__*/_jsx(Drawer.Handle, {
894
1093
  style: handleStyle
895
- }), scrollable ?
1094
+ }), useSizedLayout ?
896
1095
  /*#__PURE__*/
897
1096
  // vaul wraps children in `[data-vaul-auto-size-wrapper]` (display:
898
1097
  // flow-root) which doesn't honor descendant flex layout. Use an
899
1098
  // absolute fill sized to the visible portion (via vaul's
900
1099
  // `--snap-point-height` var) so the inner flex column has a
901
- // definite height for the scroll container's flex:1 to fill.
1100
+ // definite height for flex layouts to fill mirrors native, where
1101
+ // the container is sized to the sheet's visible height per detent.
902
1102
  _jsxs("div", {
903
- style: scrollableLayoutStyle,
1103
+ ref: sizedLayoutRef,
1104
+ style: sizedLayoutStyle,
1105
+ "data-vaul-scroll-locked": isScrollLocked ? '' : undefined,
904
1106
  children: [header && /*#__PURE__*/_jsx(View, {
905
1107
  ref: headerElRef,
906
- style: headerStyle,
1108
+ style: resolvedHeaderStyle,
907
1109
  onLayout: handleHeaderLayout,
908
1110
  children: /*#__PURE__*/isValidElement(header) ? header : /*#__PURE__*/createElement(header)
909
- }), /*#__PURE__*/_jsx("div", {
910
- style: scrollableContainerStyle,
911
- "data-vaul-scroll-locked": isScrollLocked ? '' : undefined,
912
- children: /*#__PURE__*/_jsx(View, {
913
- ref: contentRef,
914
- style: style,
915
- children: children
916
- })
1111
+ }), /*#__PURE__*/_jsx(View, {
1112
+ ref: contentRef,
1113
+ style: hasAutoDetent && hasBoundedScrollable ? [contentFillStyle, style] : style,
1114
+ children: children
1115
+ }), footer && !absoluteFooter && /*#__PURE__*/_jsx(View, {
1116
+ ref: footerElRef,
1117
+ style: [relativeFooterStyle, resolvedFooterStyle],
1118
+ onLayout: handleFooterLayout,
1119
+ children: /*#__PURE__*/isValidElement(footer) ? footer : /*#__PURE__*/createElement(footer)
917
1120
  })]
918
- }) : /*#__PURE__*/_jsxs(_Fragment, {
1121
+ }) :
1122
+ /*#__PURE__*/
1123
+ // Natural flow so vaul can measure content height — required for
1124
+ // 'auto' detents and form-sheet content-fit sizing.
1125
+ _jsxs(_Fragment, {
919
1126
  children: [header && /*#__PURE__*/_jsx(View, {
920
1127
  ref: headerElRef,
921
- style: headerStyle,
1128
+ style: resolvedHeaderStyle,
922
1129
  onLayout: handleHeaderLayout,
923
1130
  children: /*#__PURE__*/isValidElement(header) ? header : /*#__PURE__*/createElement(header)
924
1131
  }), /*#__PURE__*/_jsx(View, {
925
1132
  ref: contentRef,
926
1133
  style: style,
927
1134
  children: children
1135
+ }), footer && !absoluteFooter && /*#__PURE__*/_jsx(View, {
1136
+ ref: footerElRef,
1137
+ style: resolvedFooterStyle,
1138
+ onLayout: handleFooterLayout,
1139
+ children: /*#__PURE__*/isValidElement(footer) ? footer : /*#__PURE__*/createElement(footer)
928
1140
  })]
929
1141
  })]
930
1142
  })]
@@ -937,21 +1149,33 @@ const overlayStyle = {
937
1149
  inset: 0,
938
1150
  backgroundColor: 'rgba(0, 0, 0, 0.5)'
939
1151
  };
940
- const scrollableLayoutStyle = {
1152
+
1153
+ // Pinned to the sheet's bottom edge — takes up layout space below the content
1154
+ const relativeFooterStyle = {
1155
+ marginTop: 'auto'
1156
+ };
1157
+
1158
+ // Floats over the content so it doesn't take up layout space
1159
+ const absoluteHeaderStyle = {
1160
+ position: 'absolute',
1161
+ top: 0,
1162
+ left: 0,
1163
+ right: 0,
1164
+ zIndex: 1
1165
+ };
1166
+ const SIZED_LAYOUT_HEIGHT = 'calc(100% - var(--snap-point-height, 0px))';
1167
+ const sizedLayoutStyle = {
941
1168
  position: 'absolute',
942
1169
  top: 0,
943
1170
  left: 0,
944
1171
  right: 0,
945
- height: 'calc(100% - var(--snap-point-height, 0px))',
1172
+ height: SIZED_LAYOUT_HEIGHT,
946
1173
  display: 'flex',
947
1174
  flexDirection: 'column'
948
1175
  };
949
- const scrollableContainerStyle = {
1176
+ const contentFillStyle = {
950
1177
  flex: 1,
951
- minHeight: 0,
952
- overflowY: 'auto',
953
- overscrollBehavior: 'contain',
954
- touchAction: 'pan-y'
1178
+ minHeight: 0
955
1179
  };
956
1180
  const visuallyHiddenStyle = {
957
1181
  position: 'absolute',