@lodev09/react-native-true-sheet 4.0.0-beta.14 → 4.0.0-beta.16

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 (74) hide show
  1. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +28 -6
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +99 -7
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +12 -0
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +11 -1
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +18 -7
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +40 -39
  7. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +18 -8
  8. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +14 -8
  9. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +8 -1
  10. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +32 -5
  11. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +3 -0
  12. package/ios/TrueSheetContainerView.h +17 -4
  13. package/ios/TrueSheetContainerView.mm +23 -6
  14. package/ios/TrueSheetContentView.h +13 -1
  15. package/ios/TrueSheetContentView.mm +93 -35
  16. package/ios/TrueSheetFooterView.mm +46 -16
  17. package/ios/TrueSheetView.mm +30 -28
  18. package/ios/TrueSheetViewController.h +21 -1
  19. package/ios/TrueSheetViewController.mm +54 -16
  20. package/ios/core/TrueSheetDetentCalculator.h +6 -1
  21. package/ios/core/TrueSheetDetentCalculator.mm +9 -4
  22. package/ios/core/TrueSheetKeyboardObserver.mm +2 -0
  23. package/lib/module/TrueSheet.js +8 -5
  24. package/lib/module/TrueSheet.js.map +1 -1
  25. package/lib/module/TrueSheet.web.js +93 -218
  26. package/lib/module/TrueSheet.web.js.map +1 -1
  27. package/lib/module/TrueSheetPeek.web.js +20 -15
  28. package/lib/module/TrueSheetPeek.web.js.map +1 -1
  29. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +5 -3
  30. package/lib/module/navigation/expo-router/index.js +5 -4
  31. package/lib/module/navigation/expo-router/index.js.map +1 -1
  32. package/lib/module/web/constants.js +1 -1
  33. package/lib/module/web/constants.js.map +1 -1
  34. package/lib/module/web/layout.js +168 -0
  35. package/lib/module/web/layout.js.map +1 -0
  36. package/lib/module/web/vaul/helpers.js +1 -0
  37. package/lib/module/web/vaul/helpers.js.map +1 -1
  38. package/lib/module/web/vaul/index.js +9 -4
  39. package/lib/module/web/vaul/index.js.map +1 -1
  40. package/lib/module/web/vaul/use-snap-points.js +6 -8
  41. package/lib/module/web/vaul/use-snap-points.js.map +1 -1
  42. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  43. package/lib/typescript/src/TrueSheet.types.d.ts +64 -25
  44. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  45. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  46. package/lib/typescript/src/TrueSheetPeek.web.d.ts +8 -4
  47. package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
  48. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +5 -3
  49. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  50. package/lib/typescript/src/navigation/expo-router/index.d.ts +5 -3
  51. package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -1
  52. package/lib/typescript/src/navigation/types.d.ts +1 -1
  53. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  54. package/lib/typescript/src/web/constants.d.ts +1 -1
  55. package/lib/typescript/src/web/constants.d.ts.map +1 -1
  56. package/lib/typescript/src/web/layout.d.ts +19 -0
  57. package/lib/typescript/src/web/layout.d.ts.map +1 -0
  58. package/lib/typescript/src/web/vaul/helpers.d.ts +1 -0
  59. package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
  60. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
  61. package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
  62. package/package.json +7 -6
  63. package/src/TrueSheet.tsx +7 -4
  64. package/src/TrueSheet.types.ts +67 -25
  65. package/src/TrueSheet.web.tsx +132 -263
  66. package/src/TrueSheetPeek.web.tsx +25 -23
  67. package/src/fabric/TrueSheetViewNativeComponent.ts +5 -3
  68. package/src/navigation/expo-router/index.ts +8 -11
  69. package/src/navigation/types.ts +3 -2
  70. package/src/web/constants.ts +1 -1
  71. package/src/web/layout.ts +196 -0
  72. package/src/web/vaul/helpers.ts +5 -0
  73. package/src/web/vaul/index.tsx +21 -4
  74. package/src/web/vaul/use-snap-points.ts +6 -14
@@ -5,8 +5,9 @@ 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 { observeSheetLayout } from "./web/layout.js";
10
11
  import { Drawer } from "./web/vaul/index.js";
11
12
  import { DEFAULT_PEEK_HEIGHT, DRAG_CLASS, TRANSITIONS } from "./web/vaul/constants.js";
12
13
 
@@ -14,7 +15,7 @@ import { DEFAULT_PEEK_HEIGHT, DRAG_CLASS, TRANSITIONS } from "./web/vaul/constan
14
15
  // ScrollView refs expose the scroll node directly (with helpers attached);
15
16
  // list refs (e.g. FlatList) expose it via getScrollableNode(). The node's
16
17
  // first child is the content container.
17
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
18
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
19
  const getScrollableElement = scrollable => {
19
20
  if (!scrollable) return null;
20
21
  if (scrollable instanceof HTMLElement) return scrollable;
@@ -32,8 +33,8 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
32
33
  backgroundColor: backgroundColorProp,
33
34
  maxContentHeight,
34
35
  maxContentWidth,
35
- anchor = 'center',
36
- anchorOffset = DEFAULT_ANCHOR_OFFSET,
36
+ placement = 'automatic',
37
+ placementOffset = DEFAULT_PLACEMENT_OFFSET,
37
38
  scrollableRef,
38
39
  scrollableOptions,
39
40
  grabber = true,
@@ -104,7 +105,6 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
104
105
  }, [validDetents]);
105
106
  const validDetentsRef = useRef(validDetents);
106
107
  validDetentsRef.current = validDetents;
107
- const hasAutoDetent = validDetents.includes('auto');
108
108
  const handleSetActiveSnapPoint = useCallback(snapPoint => {
109
109
  setActiveSnapPoint(snapPoint == null ? null : typeof snapPoint === 'number' || snapPoint === 'auto' || snapPoint === 'peek' ? snapPoint : null);
110
110
  }, []);
@@ -201,16 +201,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
201
201
  const [peekContentHeight, setPeekContentHeight] = useState(0);
202
202
  const contentRef = useRef(null);
203
203
  const peekElRef = useRef(null);
204
- const peekContext = useMemo(() => ({
205
- contentRef,
206
- peekRef: peekElRef,
207
- setPeekContentHeight
208
- }), []);
209
-
210
- // The peek's own onLayout only fires when *it* resizes. Content added above
211
- // it moves it without resizing it, so re-measure its offset whenever the
212
- // content view's layout changes.
213
- const handleContentLayout = useCallback(() => {
204
+ const measurePeek = useCallback(() => {
214
205
  const peekEl = getDOMElement(peekElRef.current);
215
206
  const contentEl = getDOMElement(contentRef.current);
216
207
  if (peekEl && contentEl) {
@@ -218,11 +209,32 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
218
209
  }
219
210
  }, []);
220
211
 
212
+ // Last-attached wins — during a screen swap the incoming peek mounts before
213
+ // the outgoing one unmounts, so the outgoing detach must not clear it.
214
+ const peekContext = useMemo(() => ({
215
+ contentRef,
216
+ attachPeek: view => {
217
+ peekElRef.current = view;
218
+ measurePeek();
219
+ },
220
+ detachPeek: view => {
221
+ if (peekElRef.current !== view) return;
222
+ peekElRef.current = null;
223
+ setPeekContentHeight(0);
224
+ },
225
+ measurePeek: view => {
226
+ if (peekElRef.current === view) measurePeek();
227
+ }
228
+ }), [measurePeek]);
229
+
230
+ // The peek's own onLayout only fires when *it* resizes. Content added above
231
+ // it moves it without resizing it, so re-measure its offset whenever the
232
+ // content view's layout changes.
233
+ const handleContentLayout = measurePeek;
234
+
221
235
  // An absolute (floating) header overlaps the content, so it contributes no
222
236
  // height to the 'auto' detent measurement.
223
237
  const absoluteHeader = headerOptions?.position === 'absolute';
224
- const absoluteHeaderRef = useRef(absoluteHeader);
225
- absoluteHeaderRef.current = absoluteHeader;
226
238
  const resolvedHeaderStyle = absoluteHeader ? [absoluteHeaderStyle, headerStyle] : headerStyle;
227
239
 
228
240
  // Same for an absolute (floating) footer — rendered via vaul's
@@ -249,114 +261,54 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
249
261
  // A relative footer is laid out below the content, so it's pushed off-screen
250
262
  // at the peek detent and contributes no height.
251
263
  const peekHeight = (header ? headerHeight : 0) + (footer && absoluteFooter ? footerHeight : 0) + peekContentHeight || DEFAULT_PEEK_HEIGHT;
252
-
253
- // Web mirror of native's scrollable handling for 'auto' detents: a plugged
254
- // scrollable keeps the sized (bounded) layout so its viewport is capped to
255
- // the visible sheet and can scroll, while the 'auto' height is measured with
256
- // the viewport replaced by the scrollable's content size — mirrors native
257
- // `naturalHeight`.
258
- const [hasBoundedScrollable, setHasBoundedScrollable] = useState(false);
259
- const [scrollableAutoHeight, setScrollableAutoHeight] = useState(0);
260
- const detectedScrollerRef = useRef(null);
261
-
262
- // Natural content height (header + content + footer, with a detected
263
- // scrollable's viewport replaced by its content size) — the height the
264
- // content wants regardless of the sheet's bounds.
265
- const measureNaturalHeight = useCallback(() => {
266
- const contentEl = getDOMElement(contentRef.current);
267
- if (!contentEl || !contentEl.isConnected) return 0;
268
- const headerEl = absoluteHeaderRef.current ? null : getDOMElement(headerElRef.current);
269
- const footerEl = absoluteFooterRef.current ? null : getDOMElement(footerElRef.current);
270
- let height = (headerEl?.offsetHeight ?? 0) + (footerEl?.offsetHeight ?? 0) + contentEl.offsetHeight;
271
- const scroller = detectedScrollerRef.current;
272
- const scrollContent = scroller?.firstElementChild;
273
- if (scroller?.isConnected && scrollContent instanceof HTMLElement) {
274
- height += scrollContent.offsetHeight - scroller.clientHeight;
275
- }
276
- return Math.max(0, height);
277
- }, []);
264
+ const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
265
+ const naturalHeightRef = useRef(0);
266
+ const insetBehavior = scrollableOptions?.contentInsetAdjustment ?? 'automatic';
267
+ const footerInsetAdjustment = Boolean(footer) && absoluteFooter && (insetBehavior === 'automatic' || insetBehavior === 'footer');
268
+ const measureContent = validDetents.includes('auto') || isFormSheet;
278
269
  useEffect(() => {
279
- if (!isOpen || !hasAutoDetent) return undefined;
270
+ if (!isOpen) return undefined;
280
271
  let canceled = false;
281
272
  let rafId = 0;
282
- let mutationObserver = null;
283
- let resizeObserver = null;
273
+ let observer;
284
274
  const attach = () => {
285
275
  if (canceled) return;
286
- const drawerEl = drawerContentRef.current;
287
- if (!drawerEl || !drawerEl.isConnected) {
276
+ const layoutEl = sizedLayoutRef.current;
277
+ const drawer = drawerContentRef.current;
278
+ if (!drawer || !layoutEl?.isConnected) {
288
279
  // Radix Presence defers the portal mount; poll until the drawer is live.
289
280
  rafId = window.requestAnimationFrame(attach);
290
281
  return;
291
282
  }
292
- const measure = () => setScrollableAutoHeight(measureNaturalHeight());
293
- let observed = null;
294
-
295
- // (Re)resolve the plugged scrollable and observe the nodes whose size
296
- // feeds the natural height — content growth inside a bounded scroller is
297
- // invisible to the sheet's own layout (mirrors native's contentSize
298
- // observation). Resolves the content node live: the layout-branch swap
299
- // (natural flow ↔ sized) remounts it.
300
- const observe = () => {
301
- const contentEl = getDOMElement(contentRef.current);
302
- const headerEl = getDOMElement(headerElRef.current);
303
-
304
- // Skip mutations that don't change the observed topology — e.g. a
305
- // virtualized list mounting rows inside the plugged scroller. Size
306
- // changes are already covered by the ResizeObserver.
307
- if (observed && observed.contentEl === contentEl && contentEl?.isConnected && observed.headerEl === headerEl && observed.scroller.isConnected && observed.scrollContent === observed.scroller.firstElementChild) {
308
- return;
283
+ observer = observeSheetLayout({
284
+ drawer,
285
+ layout: layoutEl,
286
+ getContent: () => getDOMElement(contentRef.current),
287
+ getHeader: () => getDOMElement(headerElRef.current),
288
+ getFooter: () => getDOMElement(footerElRef.current),
289
+ getScrollable: () => getScrollableElement(scrollableRef?.current),
290
+ absoluteHeader,
291
+ absoluteFooter,
292
+ footerInsetAdjustment,
293
+ measureContent,
294
+ onHeightChange: height => {
295
+ naturalHeightRef.current = height;
296
+ setMeasuredContentHeight(height);
309
297
  }
310
- const resolved = contentEl && contentEl.isConnected ? getScrollableElement(scrollableRef?.current) : null;
311
- const scroller = resolved?.isConnected ? resolved : null;
312
- detectedScrollerRef.current = scroller;
313
- setHasBoundedScrollable(scroller != null);
314
- observed = scroller ? {
315
- contentEl,
316
- headerEl,
317
- scroller,
318
- scrollContent: scroller.firstElementChild
319
- } : null;
320
- resizeObserver?.disconnect();
321
- resizeObserver = new ResizeObserver(measure);
322
- if (contentEl?.isConnected) resizeObserver.observe(contentEl);
323
- if (headerEl) resizeObserver.observe(headerEl);
324
- if (scroller) {
325
- resizeObserver.observe(scroller);
326
- if (scroller.firstElementChild) resizeObserver.observe(scroller.firstElementChild);
327
- }
328
- measure();
329
- };
330
- observe();
331
- // Watch the whole drawer subtree — catches the scrollable mounting/
332
- // unmounting AND the layout-branch swap, which remounts the content node
333
- // (a content-scoped observer would go stale after the swap).
334
- mutationObserver = new MutationObserver(observe);
335
- mutationObserver.observe(drawerEl, {
336
- childList: true,
337
- subtree: true
338
298
  });
339
299
  };
340
300
  rafId = window.requestAnimationFrame(attach);
341
301
  return () => {
342
302
  canceled = true;
343
303
  window.cancelAnimationFrame(rafId);
344
- mutationObserver?.disconnect();
345
- resizeObserver?.disconnect();
304
+ observer?.disconnect();
346
305
  };
347
- }, [isOpen, hasAutoDetent, measureNaturalHeight, scrollableRef]);
306
+ }, [isOpen, absoluteHeader, absoluteFooter, footerInsetAdjustment, measureContent, scrollableRef]);
348
307
 
349
308
  // Below the last detent a vertical touch pan moves the sheet, not the
350
309
  // content — `[data-vaul-scroll-locked]` disables vertical touch panning on
351
310
  // the scroll container and everything inside it (see vaul/style.css).
352
311
  const isScrollLocked = validDetents.length > 0 && activeSnapPoint !== validDetents[validDetents.length - 1];
353
-
354
- // Vaul measures the auto-size wrapper's offsetHeight (always, post fork).
355
- // Track it here so the form sheet can size its card to fit content,
356
- // clamped between a minimum ratio of the viewport and a maximum derived
357
- // from `detachedOffset` (the breathing room left at top + bottom of the
358
- // floating card).
359
- const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
360
312
  const effectiveMaxContentHeight = useMemo(() => {
361
313
  if (maxContentHeight !== undefined) return maxContentHeight;
362
314
  if (!isFormSheet) return undefined;
@@ -419,16 +371,14 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
419
371
  effectiveDetachedOffset,
420
372
  effectiveMaxContentHeight,
421
373
  peekHeight,
422
- peekContentHeight,
423
- measuredContentHeight
374
+ peekContentHeight
424
375
  });
425
376
  geometryInputsRef.current = {
426
377
  effectiveDetached,
427
378
  effectiveDetachedOffset,
428
379
  effectiveMaxContentHeight,
429
380
  peekHeight,
430
- peekContentHeight,
431
- measuredContentHeight
381
+ peekContentHeight
432
382
  };
433
383
  const computeDetentGeometry = useCallback(() => {
434
384
  const inputs = geometryInputsRef.current;
@@ -436,12 +386,9 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
436
386
  const effectiveH = inputs.effectiveDetached ? windowH - inputs.effectiveDetachedOffset : windowH;
437
387
  // Matches vaul's height ceiling: min(effectiveH, maxContentHeight).
438
388
  const ceiling = inputs.effectiveMaxContentHeight !== undefined ? Math.min(effectiveH, inputs.effectiveMaxContentHeight) : effectiveH;
439
- // 'auto' resolves to the content's natural height. Read it live — the
440
- // `measuredContentHeight` state lags mount by a frame (vaul's initial
441
- // ref-callback measure runs while the portal subtree is still detached,
442
- // so it reads 0 until the ResizeObserver fires). Falls back to the state
443
- // value, then to vaul's pre-measure fallback (effectiveHeight / 2).
444
- const measuredHeight = measureNaturalHeight() || inputs.measuredContentHeight;
389
+ // Reuse the intrinsic measurement during position events so animation
390
+ // frames don't clone the content tree again.
391
+ const measuredHeight = naturalHeightRef.current;
445
392
  const autoHeight = Math.min(measuredHeight > 0 ? measuredHeight : effectiveH / 2, ceiling);
446
393
 
447
394
  // 'peek' is measured live from the DOM (offsetHeight is layout-based, so
@@ -472,7 +419,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
472
419
  positions,
473
420
  values
474
421
  };
475
- }, [measureNaturalHeight]);
422
+ }, []);
476
423
 
477
424
  // Detent info for lifecycle events. Position/detent come from the active
478
425
  // detent's target geometry — not the live DOM rect — so willPresent (drawer
@@ -559,46 +506,9 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
559
506
  detent: values[count - 1]
560
507
  };
561
508
  }, [computeDetentGeometry]);
562
-
563
- // Mirror native synchronous layout: while dragging (and through the post-
564
- // release settle) the sized layout tracks the sheet's visible height in
565
- // realtime, clamped to the smallest detent — dragging below it slides the
566
- // sheet out without resizing. Once settled, the inline height is cleared so
567
- // the CSS calc() owns sizing again (adapts to viewport resizes at rest).
568
509
  const sizedLayoutRef = useRef(null);
569
510
  const isDraggingRef = useRef(false);
570
- const isSettlingAfterDragRef = useRef(false);
571
- const updateSizedLayoutHeight = useCallback(position => {
572
- const node = sizedLayoutRef.current;
573
- if (!node) return;
574
- const {
575
- effectiveH,
576
- ceiling,
577
- positions
578
- } = computeDetentGeometry();
579
- if (positions.length === 0) return;
580
- if (isSettlingAfterDragRef.current) {
581
- const snap = activeSnapPointRef.current;
582
- const index = snap != null ? validDetentsRef.current.indexOf(snap) : -1;
583
- const target = index >= 0 ? positions[index] : null;
584
- if (target != null && Math.abs(position - target) < 1) {
585
- isSettlingAfterDragRef.current = false;
586
- // Restore the calc — height is an inline style, so clearing it would
587
- // leave the div unsized (React won't re-apply it without a render)
588
- node.style.height = SIZED_LAYOUT_HEIGHT;
589
- return;
590
- }
591
- }
592
-
593
- // positions[0] is the smallest detent's top-Y — its height is the clamp floor.
594
- const minHeight = effectiveH - positions[0];
595
- const height = Math.min(Math.max(effectiveH - position, minHeight), ceiling);
596
- node.style.height = `${height}px`;
597
- }, [computeDetentGeometry]);
598
511
  const handlePositionChange = useCallback(position => {
599
- if (isDraggingRef.current || isSettlingAfterDragRef.current) {
600
- updateSizedLayoutHeight(position);
601
- }
602
512
  const {
603
513
  index,
604
514
  detent
@@ -611,7 +521,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
611
521
  realtime: true
612
522
  }
613
523
  });
614
- }, [interpolateFromPosition, updateSizedLayoutHeight]);
524
+ }, [interpolateFromPosition]);
615
525
 
616
526
  // Fire onMount once after first render. React-mount is the earliest point
617
527
  // the component is ready for imperative calls, matching the native
@@ -757,12 +667,9 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
757
667
  nativeEvent: computeDetentInfo(true)
758
668
  });
759
669
  }, [computeDetentInfo]);
760
- const handleRelease = useCallback((_event, open) => {
670
+ const handleRelease = useCallback(() => {
761
671
  if (!isDraggingRef.current) return;
762
672
  isDraggingRef.current = false;
763
- // Track the sized layout through the settle animation only when the sheet
764
- // stays open — a dismissal slides out at the min-clamped height.
765
- isSettlingAfterDragRef.current = open;
766
673
  onDragEndRef.current?.({
767
674
  nativeEvent: computeDetentInfo(true)
768
675
  });
@@ -784,6 +691,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
784
691
  // Mirror Android: translate this sheet down to match the deepest descendant's
785
692
  // top so the whole stack visually aligns. Cascades because every ancestor
786
693
  // re-runs whenever the stack (and thus its descendants) changes.
694
+ const wasCascadedRef = useRef(false);
787
695
  useEffect(() => {
788
696
  const parent = drawerContentRef.current;
789
697
  if (!parent) return;
@@ -794,7 +702,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
794
702
  // out — nothing to align with anymore.
795
703
  if (!isOpen) return;
796
704
  const parentWrapper = parent.closest('[data-vaul-detached-wrapper]');
797
- const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
705
+ const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')}), --snap-point-height ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
798
706
  const wrapperTransition = `clip-path ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
799
707
  const CLIP_NONE = 'inset(0px round 0px)';
800
708
 
@@ -817,11 +725,14 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
817
725
  parentWrapper.style.clipPath = next;
818
726
  };
819
727
  if (descendants.length === 0) {
728
+ if (!wasCascadedRef.current) return;
729
+ wasCascadedRef.current = false;
820
730
  parent.style.transition = transition;
821
731
  parent.style.transform = '';
822
732
  setClip(CLIP_NONE);
823
733
  return;
824
734
  }
735
+ wasCascadedRef.current = true;
825
736
 
826
737
  // Track the immediate child's *actual* top: its cascade transform when it
827
738
  // was itself pushed behind a grandchild, else its own snap point. Mirrors
@@ -985,11 +896,6 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
985
896
  window.cancelAnimationFrame(rafId);
986
897
  };
987
898
  }, [isOpen, descendants.length]);
988
-
989
- // Definite-height flex layout (per-detent sizing) unless content must be
990
- // measured in natural flow: 'auto' detents (without a plugged scrollable —
991
- // those measure via `measureNaturalHeight`) and form-sheet content-fit sizing.
992
- const useSizedLayout = (!hasAutoDetent || hasBoundedScrollable) && !isFormSheet;
993
899
  const effectiveCornerRadius = cornerRadius ?? DEFAULT_CORNER_RADIUS;
994
900
 
995
901
  // Shadow cast upward from the sheet's top edge toward the background. Matches
@@ -1023,11 +929,11 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
1023
929
  // Lift content above iOS home indicator / bottom safe area when enabled.
1024
930
  // A relative footer owns the inset instead (see resolvedFooterStyle); an
1025
931
  // absolute footer floats over the content, so the content keeps its lift.
1026
- // A plugged scrollable keeps the lift too (contentInsetAdjustmentBehavior,
1027
- // default on) — disable it to scroll edge-to-edge behind the indicator
1028
- // with user-applied content padding.
1029
- paddingBottom: insetAdjustment === 'automatic' && !(footerOwnsInset && !absoluteFooter) && (!scrollableRef || (scrollableOptions?.contentInsetAdjustmentBehavior ?? true)) ? 'env(safe-area-inset-bottom, 0px)' : 0
1030
- }), [backgroundColor, effectiveCornerRadius, insetAdjustment, footerOwnsInset, absoluteFooter, scrollableRef, scrollableOptions]);
932
+ // A plugged scrollable keeps the lift too (contentInsetAdjustment
933
+ // 'automatic' | 'safe-area') — 'footer' | 'never' scroll edge-to-edge
934
+ // behind the indicator with user-applied content padding.
935
+ paddingBottom: insetAdjustment === 'automatic' && !(footerOwnsInset && !absoluteFooter) && (!scrollableRef || insetBehavior === 'automatic' || insetBehavior === 'safe-area') ? 'env(safe-area-inset-bottom, 0px)' : 0
936
+ }), [backgroundColor, effectiveCornerRadius, insetAdjustment, footerOwnsInset, absoluteFooter, scrollableRef, insetBehavior]);
1031
937
  const defaultGrabberColor = colorScheme === 'dark' ? DEFAULT_GRABBER_COLOR_DARK : DEFAULT_GRABBER_COLOR_LIGHT;
1032
938
  const grabberHeight = grabberOptions?.height ?? DEFAULT_GRABBER_HEIGHT;
1033
939
 
@@ -1058,7 +964,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
1058
964
  // `maxContentWidth` is ignored ('form' is absolute).
1059
965
  // - presentation='page' → `maxContentWidth` (any viewport) or
1060
966
  // DEFAULT_MAX_WIDTH (tablet/landscape readability cap).
1061
- // Detached without a width constraint applies anchorOffset on both edges so
967
+ // Detached without a width constraint applies placementOffset on both edges so
1062
968
  // the floating card breathes from the viewport sides.
1063
969
  const wrapperStyle = useMemo(() => {
1064
970
  // Mobile portrait ignores width sizing entirely (matches iOS/Android:
@@ -1070,23 +976,23 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
1070
976
  const next = {};
1071
977
  if (dropShadow) next.filter = dropShadow;
1072
978
  if (!needsMargins) return next;
1073
- let marginLeft;
1074
- let marginRight;
979
+ let marginInlineStart;
980
+ let marginInlineEnd;
1075
981
  if (isFormSheet) {
1076
- marginLeft = 'auto';
1077
- marginRight = 'auto';
982
+ marginInlineStart = 'auto';
983
+ marginInlineEnd = 'auto';
1078
984
  } else if (maxWidth == null) {
1079
- marginLeft = anchorOffset;
1080
- marginRight = anchorOffset;
985
+ marginInlineStart = placementOffset;
986
+ marginInlineEnd = placementOffset;
1081
987
  } else {
1082
- marginLeft = anchor === 'left' ? anchorOffset : 'auto';
1083
- marginRight = anchor === 'right' ? anchorOffset : 'auto';
988
+ marginInlineStart = placement === 'leading' ? placementOffset : 'auto';
989
+ marginInlineEnd = placement === 'trailing' ? placementOffset : 'auto';
1084
990
  }
1085
991
  if (maxWidth != null) next.maxWidth = maxWidth;
1086
- next.marginLeft = marginLeft;
1087
- next.marginRight = marginRight;
992
+ next.marginInlineStart = marginInlineStart;
993
+ next.marginInlineEnd = marginInlineEnd;
1088
994
  return next;
1089
- }, [isLandscapeOrTablet, isFormSheet, maxContentWidth, presentation, anchor, anchorOffset, effectiveDetached, dropShadow]);
995
+ }, [isLandscapeOrTablet, isFormSheet, maxContentWidth, presentation, placement, placementOffset, effectiveDetached, dropShadow]);
1090
996
 
1091
997
  // Absolute-position the grabber so it overlays the content top-edge
1092
998
  // instead of consuming flow height — mirrors native iOS/Android, where
@@ -1128,11 +1034,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
1128
1034
  peekHeight: peekHeight,
1129
1035
  initialAnimated: initialDetentAnimated,
1130
1036
  detachedWrapperStyle: wrapperStyle,
1131
- onContentHeightChange: setMeasuredContentHeight,
1132
- contentHeight:
1133
- // Bounded scrollable → the auto-size wrapper measures 0 (sized
1134
- // layout is absolute), so feed vaul the natural height instead.
1135
- hasAutoDetent && hasBoundedScrollable && scrollableAutoHeight > 0 ? scrollableAutoHeight : undefined,
1037
+ contentHeight: measuredContentHeight,
1136
1038
  activeSnapPoint: activeSnapPoint,
1137
1039
  setActiveSnapPoint: handleSetActiveSnapPoint,
1138
1040
  ...snapPointsProps,
@@ -1158,15 +1060,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
1158
1060
  children: accessibilityOptions?.paneTitle ?? 'Sheet'
1159
1061
  }), grabber && /*#__PURE__*/_jsx(Drawer.Handle, {
1160
1062
  style: handleStyle
1161
- }), useSizedLayout ?
1162
- /*#__PURE__*/
1163
- // vaul wraps children in `[data-vaul-auto-size-wrapper]` (display:
1164
- // flow-root) which doesn't honor descendant flex layout. Use an
1165
- // absolute fill sized to the visible portion (via vaul's
1166
- // `--snap-point-height` var) so the inner flex column has a
1167
- // definite height for flex layouts to fill — mirrors native, where
1168
- // the container is sized to the sheet's visible height per detent.
1169
- _jsxs("div", {
1063
+ }), /*#__PURE__*/_jsxs("div", {
1170
1064
  ref: sizedLayoutRef,
1171
1065
  style: sizedLayoutStyle,
1172
1066
  "data-vaul-scroll-locked": isScrollLocked ? '' : undefined,
@@ -1177,7 +1071,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
1177
1071
  children: /*#__PURE__*/isValidElement(header) ? header : /*#__PURE__*/createElement(header)
1178
1072
  }), /*#__PURE__*/_jsx(View, {
1179
1073
  ref: contentRef,
1180
- style: hasAutoDetent && hasBoundedScrollable ? [contentFillStyle, style] : style,
1074
+ style: [contentStyle, style],
1181
1075
  onLayout: handleContentLayout,
1182
1076
  children: children
1183
1077
  }), footer && !absoluteFooter && /*#__PURE__*/_jsx(View, {
@@ -1186,27 +1080,6 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
1186
1080
  onLayout: handleFooterLayout,
1187
1081
  children: /*#__PURE__*/isValidElement(footer) ? footer : /*#__PURE__*/createElement(footer)
1188
1082
  })]
1189
- }) :
1190
- /*#__PURE__*/
1191
- // Natural flow so vaul can measure content height — required for
1192
- // 'auto' detents and form-sheet content-fit sizing.
1193
- _jsxs(_Fragment, {
1194
- children: [header && /*#__PURE__*/_jsx(View, {
1195
- ref: headerElRef,
1196
- style: resolvedHeaderStyle,
1197
- onLayout: handleHeaderLayout,
1198
- children: /*#__PURE__*/isValidElement(header) ? header : /*#__PURE__*/createElement(header)
1199
- }), /*#__PURE__*/_jsx(View, {
1200
- ref: contentRef,
1201
- style: style,
1202
- onLayout: handleContentLayout,
1203
- children: children
1204
- }), footer && !absoluteFooter && /*#__PURE__*/_jsx(View, {
1205
- ref: footerElRef,
1206
- style: resolvedFooterStyle,
1207
- onLayout: handleFooterLayout,
1208
- children: /*#__PURE__*/isValidElement(footer) ? footer : /*#__PURE__*/createElement(footer)
1209
- })]
1210
1083
  })]
1211
1084
  })]
1212
1085
  })
@@ -1240,10 +1113,12 @@ const sizedLayoutStyle = {
1240
1113
  right: 0,
1241
1114
  height: SIZED_LAYOUT_HEIGHT,
1242
1115
  display: 'flex',
1243
- flexDirection: 'column'
1116
+ flexDirection: 'column',
1117
+ boxSizing: 'border-box',
1118
+ paddingBottom: 'inherit'
1244
1119
  };
1245
- const contentFillStyle = {
1246
- flex: 1,
1120
+ const contentStyle = {
1121
+ flexShrink: 1,
1247
1122
  minHeight: 0
1248
1123
  };
1249
1124
  const visuallyHiddenStyle = {