@lodev09/react-native-true-sheet 3.11.0-beta.1 → 3.11.0-beta.10

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 (41) hide show
  1. package/LICENSE +1 -1
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +3 -3
  3. package/ios/TrueSheetFooterView.mm +10 -1
  4. package/ios/TrueSheetView.mm +1 -1
  5. package/ios/TrueSheetViewController.h +1 -1
  6. package/ios/TrueSheetViewController.mm +10 -4
  7. package/lib/module/TrueSheet.js +2 -2
  8. package/lib/module/TrueSheet.js.map +1 -1
  9. package/lib/module/TrueSheet.web.js +222 -50
  10. package/lib/module/TrueSheet.web.js.map +1 -1
  11. package/lib/module/TrueSheetProvider.web.js +5 -2
  12. package/lib/module/TrueSheetProvider.web.js.map +1 -1
  13. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +1 -1
  14. package/lib/module/web/constants.js +1 -1
  15. package/lib/module/web/constants.js.map +1 -1
  16. package/lib/module/web/vaul/index.js +88 -133
  17. package/lib/module/web/vaul/index.js.map +1 -1
  18. package/lib/module/web/vaul/style.css +1 -1
  19. package/lib/typescript/src/TrueSheet.types.d.ts +6 -4
  20. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  21. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  22. package/lib/typescript/src/TrueSheetProvider.web.d.ts +2 -1
  23. package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -1
  24. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +1 -1
  25. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  26. package/lib/typescript/src/navigation/types.d.ts +1 -1
  27. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  28. package/lib/typescript/src/web/constants.d.ts +1 -1
  29. package/lib/typescript/src/web/constants.d.ts.map +1 -1
  30. package/lib/typescript/src/web/vaul/index.d.ts +7 -1
  31. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
  32. package/package.json +6 -1
  33. package/src/TrueSheet.tsx +2 -2
  34. package/src/TrueSheet.types.ts +6 -4
  35. package/src/TrueSheet.web.tsx +220 -52
  36. package/src/TrueSheetProvider.web.tsx +11 -2
  37. package/src/fabric/TrueSheetViewNativeComponent.ts +1 -1
  38. package/src/navigation/types.ts +1 -1
  39. package/src/web/constants.ts +1 -1
  40. package/src/web/vaul/index.tsx +118 -146
  41. package/src/web/vaul/style.css +1 -1
@@ -76,8 +76,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
76
76
  footer,
77
77
  footerStyle,
78
78
  scrollable = false,
79
- scrollableOptions,
80
- pageSizing = true,
79
+ presentation = 'page',
81
80
  detached = false,
82
81
  detachedOffset = DEFAULT_DETACHED_OFFSET,
83
82
  elevation = 4,
@@ -116,10 +115,11 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
116
115
 
117
116
  const { width: windowWidth, height: windowHeight } = useWindowDimensions();
118
117
  const isLandscapeOrTablet = windowWidth >= 600 || windowWidth > windowHeight;
118
+ const isFormSheet = isLandscapeOrTablet && presentation === 'form';
119
119
 
120
- // pageSizing=false implies a floating/detached sheet on web — mirrors iOS
120
+ // presentation='form' implies a floating/detached sheet on web — mirrors iOS
121
121
  // form-sheet semantics where the sheet is never edge-attached.
122
- const effectiveDetached = !pageSizing || detached;
122
+ const effectiveDetached = presentation === 'form' || detached;
123
123
 
124
124
  const colorScheme = useColorScheme();
125
125
  const backgroundColor =
@@ -171,6 +171,17 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
171
171
  // in another screen's tree when navigating) should not close the drawer.
172
172
  if (portalContainer && !portalContainer.contains(target)) {
173
173
  e.preventDefault();
174
+ return;
175
+ }
176
+ // The footer is rendered via vaul's `detachedSiblings` as a sibling of
177
+ // Drawer.Content inside [data-vaul-detached-wrapper], so Radix treats
178
+ // clicks on it as "outside" the content. Don't dismiss for clicks that
179
+ // landed inside the wrapper.
180
+ if (target instanceof Element) {
181
+ const wrapper = drawerContentRef.current?.closest('[data-vaul-detached-wrapper]');
182
+ if (wrapper && wrapper.contains(target)) {
183
+ e.preventDefault();
184
+ }
174
185
  }
175
186
  };
176
187
 
@@ -370,8 +381,15 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
370
381
 
371
382
  const present = !wasOpen && isOpen;
372
383
  if (present) {
384
+ // Pair willFocus with willPresent on initial present — mirrors native
385
+ // iOS where viewWillAppear dispatches both. The descendant-stack focus
386
+ // effect handles subsequent gained/lost transitions.
373
387
  onWillPresentRef.current?.({ nativeEvent: computeDetentInfo() } as WillPresentEvent);
388
+ onWillFocusRef.current?.({ nativeEvent: null } as WillFocusEvent);
374
389
  } else if (wasOpen && !isOpen) {
390
+ // Pair willBlur with willDismiss on dismiss — mirrors native iOS
391
+ // emitWillDismissEvents (blur fires before dismiss).
392
+ onWillBlurRef.current?.({ nativeEvent: null } as WillBlurEvent);
375
393
  onWillDismissRef.current?.({ nativeEvent: null } as WillDismissEvent);
376
394
  } else {
377
395
  return undefined;
@@ -380,7 +398,9 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
380
398
  const fireDone = () => {
381
399
  if (present) {
382
400
  onDidPresentRef.current?.({ nativeEvent: computeDetentInfo() } as DidPresentEvent);
401
+ onDidFocusRef.current?.({ nativeEvent: null } as DidFocusEvent);
383
402
  } else {
403
+ onDidBlurRef.current?.({ nativeEvent: null } as DidBlurEvent);
384
404
  onDidDismissRef.current?.({ nativeEvent: null } as DidDismissEvent);
385
405
  }
386
406
  };
@@ -463,7 +483,8 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
463
483
  const { isNested, dismissAbove, descendants } = useSheetStack(
464
484
  methodsRef,
465
485
  drawerContentRef,
466
- isOpen
486
+ isOpen,
487
+ isFormSheet
467
488
  );
468
489
  dismissAboveRef.current = dismissAbove;
469
490
 
@@ -473,28 +494,97 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
473
494
  useEffect(() => {
474
495
  const parent = drawerContentRef.current;
475
496
  if (!parent) return;
497
+ // Skip while dismissing: this sheet's stack pop changes `descendants`,
498
+ // which would re-fire the effect and write `wrapper.style.transition =
499
+ // 'clip-path …'`, clobbering vaul's just-written `'transform …'` for the
500
+ // dismiss animation. Vaul fully owns this sheet's transitions on the way
501
+ // out — nothing to align with anymore.
502
+ if (!isOpen) return;
503
+ const parentWrapper = parent.closest<HTMLElement>('[data-vaul-detached-wrapper]');
476
504
 
477
505
  const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
506
+ const wrapperTransition = `clip-path ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
507
+ const CLIP_NONE = 'inset(0px round 0px)';
508
+
509
+ // Animate clip-path on the wrapper. Dedupes via DOM read so repeat ticks
510
+ // (mutation observer) skip identical writes. Seeds CLIP_NONE before the
511
+ // first inset() so the browser interpolates between two inset() shapes —
512
+ // `none → inset()` interpolates inconsistently.
513
+ const setClip = (next: string) => {
514
+ if (!parentWrapper) return;
515
+ const current = parentWrapper.style.clipPath;
516
+ if (current === next) return;
517
+ if (next === CLIP_NONE && !current) return;
518
+ if (!current) {
519
+ parentWrapper.style.transition = '';
520
+ parentWrapper.style.clipPath = CLIP_NONE;
521
+ // eslint-disable-next-line no-void
522
+ void parentWrapper.offsetHeight;
523
+ }
524
+ parentWrapper.style.transition = wrapperTransition;
525
+ parentWrapper.style.clipPath = next;
526
+ };
478
527
 
479
528
  if (descendants.length === 0) {
480
529
  parent.style.transition = transition;
481
530
  parent.style.transform = '';
531
+ setClip(CLIP_NONE);
482
532
  return;
483
533
  }
484
534
 
535
+ // Track only the immediate child's snap point. Walking deeper descendants
536
+ // would push this sheet further when a grandchild opens, even when our
537
+ // own child didn't move (e.g., child skipped its cascade for a page
538
+ // grandchild) — leaving a visible gap between this sheet and its child.
485
539
  const computeTargetY = () => {
486
540
  const parentSnap = parseFloat(parent.style.getPropertyValue('--snap-point-height')) || 0;
487
- let targetY = parentSnap;
488
- for (const d of descendants) {
489
- const node = d.nodeRef.current;
490
- if (!node) continue;
491
- const snap = parseFloat(node.style.getPropertyValue('--snap-point-height')) || 0;
492
- if (snap > targetY) targetY = snap;
541
+ const node = descendants[0]?.nodeRef.current;
542
+ if (!node) return parentSnap;
543
+ const childSnap = parseFloat(node.style.getPropertyValue('--snap-point-height')) || 0;
544
+ return Math.max(parentSnap, childSnap);
545
+ };
546
+
547
+ // When a form-sheet parent has a form-sheet descendant, clip the parent to
548
+ // the child card's viewport box so it doesn't peek above/around. Only
549
+ // applies when this sheet is itself form — a page parent should remain
550
+ // visible behind/around a floating form child. Geometry comes from the
551
+ // child's inline styles (not getBoundingClientRect) to read the at-rest
552
+ // box, unskewed by vaul's slide-in.
553
+ const applyFormClip = () => {
554
+ const form = isFormSheet ? descendants.find((d) => d.isFormSheetRef.current) : undefined;
555
+ if (!form) {
556
+ setClip(CLIP_NONE);
557
+ return;
493
558
  }
494
- return targetY;
559
+ const childDrawer = form.nodeRef.current;
560
+ const childWrapper = childDrawer?.closest<HTMLElement>('[data-vaul-detached-wrapper]');
561
+ if (!parentWrapper || !childDrawer || !childWrapper) return;
562
+ const snapY = parseFloat(childDrawer.style.getPropertyValue('--snap-point-height')) || 0;
563
+ const childBottomGap = parseFloat(childWrapper.style.bottom) || 0;
564
+ const childMaxW = parseFloat(childWrapper.style.maxWidth) || window.innerWidth;
565
+ const formLeft = (window.innerWidth - childMaxW) / 2;
566
+ const formRight = (window.innerWidth + childMaxW) / 2;
567
+ const formBottom = window.innerHeight - childBottomGap;
568
+ const rect = parentWrapper.getBoundingClientRect();
569
+ const top = Math.max(0, snapY - rect.top);
570
+ const left = Math.max(0, formLeft - rect.left);
571
+ const right = Math.max(0, rect.right - formRight);
572
+ const bottom = Math.max(0, rect.bottom - formBottom);
573
+ const radius = cornerRadius ?? DEFAULT_CORNER_RADIUS;
574
+ setClip(`inset(${top}px ${right}px ${bottom}px ${left}px round ${radius}px)`);
495
575
  };
496
576
 
497
577
  const apply = () => {
578
+ applyFormClip();
579
+ // Mirror iOS: a page-sheet child fully covers a form-sheet parent, so the
580
+ // cascade push-down has no visible effect — and would briefly peek above
581
+ // the page during the present animation. Leave the parent put.
582
+ const child = descendants[0];
583
+ if (isFormSheet && child && !child.isFormSheetRef.current) {
584
+ parent.style.transition = transition;
585
+ parent.style.transform = '';
586
+ return;
587
+ }
498
588
  const targetY = computeTargetY();
499
589
  const match = parent.style.transform.match(/translate3d\([^,]*,\s*(-?\d*\.?\d+)px/);
500
590
  const currentY = match ? parseFloat(match[1]!) : 0;
@@ -514,7 +604,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
514
604
  cancelAnimationFrame(raf);
515
605
  observer.disconnect();
516
606
  };
517
- }, [descendants, activeSnapPoint]);
607
+ }, [descendants, activeSnapPoint, cornerRadius, isFormSheet, isOpen]);
518
608
 
519
609
  // Focus/blur events fire when a descendant sheet appears on top of this one
520
610
  // (blur) or when all descendants are dismissed (focus). will-events fire
@@ -583,8 +673,17 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
583
673
  // Shadow cast upward from the sheet's top edge toward the background. Matches
584
674
  // Android's `elevation` semantics roughly — the sheet "lifts" off whatever is
585
675
  // behind it. Scales linearly so higher elevation reads as more separation.
586
- const boxShadow =
587
- elevation > 0 ? `0 ${-elevation}px ${elevation * 3}px rgba(0, 0, 0, 0.15)` : undefined;
676
+ // Applied to the vaul wrapper (not the drawer) as `filter: drop-shadow`: the
677
+ // wrapper clips the drawer (overflow: hidden + contain: paint), which would
678
+ // cut off `box-shadow` on the drawer at the wrapper edges — visible in
679
+ // detached mode (bottom blur clipped in the floating gap) and when the
680
+ // wrapper is narrowed by maxWidth/anchor margins (lateral blur clipped at
681
+ // wrapper edges). drop-shadow on the wrapper follows the post-clip silhouette
682
+ // and isn't clipped by the wrapper itself.
683
+ const dropShadow =
684
+ elevation > 0
685
+ ? `drop-shadow(0 ${-elevation}px ${elevation * 3}px rgba(0, 0, 0, 0.15))`
686
+ : undefined;
588
687
 
589
688
  const mergedContentStyle = useMemo<React.CSSProperties>(
590
689
  () => ({
@@ -598,11 +697,13 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
598
697
  borderTopLeftRadius: effectiveCornerRadius,
599
698
  borderTopRightRadius: effectiveCornerRadius,
600
699
  backgroundColor: backgroundColor as string,
601
- boxShadow,
700
+ // Clip children to the rounded top so headers/content with their own
701
+ // background don't bleed past the corners.
702
+ overflow: 'hidden',
602
703
  // Lift content above iOS home indicator / bottom safe area when enabled.
603
704
  paddingBottom: insetAdjustment === 'automatic' ? 'env(safe-area-inset-bottom, 0px)' : 0,
604
705
  }),
605
- [backgroundColor, effectiveCornerRadius, boxShadow, insetAdjustment]
706
+ [backgroundColor, effectiveCornerRadius, insetAdjustment]
606
707
  );
607
708
 
608
709
  const defaultGrabberColor =
@@ -626,36 +727,68 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
626
727
  []
627
728
  );
628
729
 
629
- // Form-sheet style (iOS pageSizing=false): centered floating card with a
630
- // default width and a height capped to a fraction of the window. We reuse
631
- // the existing detached mechanic so drag/snap math stays correct — the
632
- // wrapper is bottom-attached with a computed offset that centers it
633
- // vertically.
634
- const isFormSheet = isLandscapeOrTablet && maxContentWidth == null && !pageSizing;
635
-
636
- const effectiveMaxContentHeight =
637
- maxContentHeight ?? (isFormSheet ? windowHeight * DEFAULT_FORM_SHEET_HEIGHT_RATIO : undefined);
638
-
639
- const effectiveDetachedOffset = isFormSheet
640
- ? Math.max(0, (windowHeight - (effectiveMaxContentHeight ?? 0)) / 2)
641
- : detachedOffset;
730
+ // Form-sheet style (presentation='form'): centered floating card with a
731
+ // default width and a height fit to content. We reuse the existing detached
732
+ // mechanic so drag/snap math stays correct — the wrapper is bottom-attached
733
+ // with a computed offset that centers it vertically. `presentation` is
734
+ // absolute: when 'form', `maxContentWidth` is ignored and the card uses
735
+ // DEFAULT_FORM_SHEET_WIDTH.
736
+
737
+ // Vaul measures the auto-size wrapper's offsetHeight (always, post fork).
738
+ // Track it here so the form sheet can size its card to fit content,
739
+ // clamped between a minimum ratio of the viewport and a maximum derived
740
+ // from `detachedOffset` (the breathing room left at top + bottom of the
741
+ // floating card).
742
+ const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
743
+
744
+ const effectiveMaxContentHeight = useMemo<number | undefined>(() => {
745
+ if (maxContentHeight !== undefined) return maxContentHeight;
746
+ if (!isFormSheet) return undefined;
747
+ const min = windowHeight * DEFAULT_FORM_SHEET_HEIGHT_RATIO;
748
+ const max = Math.max(min, windowHeight - 2 * detachedOffset);
749
+ if (measuredContentHeight <= 0) return min;
750
+ return Math.max(min, Math.min(measuredContentHeight, max));
751
+ }, [maxContentHeight, isFormSheet, windowHeight, detachedOffset, measuredContentHeight]);
752
+
753
+ // Center the form sheet using the actual visible drawer height. Vaul
754
+ // auto-sizes to content (capped by `maxContentHeight`), so when content is
755
+ // shorter than `effectiveMaxContentHeight`'s min-clamped floor, using that
756
+ // for the offset would push a small sheet below the viewport center.
757
+ const effectiveDetachedOffset = useMemo(() => {
758
+ if (!isFormSheet) return detachedOffset;
759
+ const max = Math.max(0, windowHeight - 2 * detachedOffset);
760
+ const visibleHeight =
761
+ measuredContentHeight > 0
762
+ ? Math.min(measuredContentHeight, max)
763
+ : (effectiveMaxContentHeight ?? 0);
764
+ return Math.max(0, (windowHeight - visibleHeight) / 2);
765
+ }, [isFormSheet, windowHeight, detachedOffset, measuredContentHeight, effectiveMaxContentHeight]);
642
766
 
643
767
  // The wrapper holds all horizontal sizing/anchoring so its rounded-bottom
644
768
  // clip (when detached) aligns with the drawer's horizontal bounds on
645
769
  // desktop — otherwise its corners sit at the far viewport edges.
646
- // Mirrors iOS setupSheetSizing: maxContentWidth wins (forces pageSizing
647
- // off). pageSizing on constrain to readable width (page-sheet);
648
- // pageSizing off with no maxContentWidth form-sheet width.
770
+ // - presentation='form' DEFAULT_FORM_SHEET_WIDTH on tablet/landscape;
771
+ // `maxContentWidth` is ignored ('form' is absolute).
772
+ // - presentation='page' `maxContentWidth` (any viewport) or
773
+ // DEFAULT_MAX_WIDTH (tablet/landscape readability cap).
649
774
  // Detached without a width constraint applies anchorOffset on both edges so
650
775
  // the floating card breathes from the viewport sides.
651
776
  const wrapperStyle = useMemo<React.CSSProperties | undefined>(() => {
777
+ // Mobile portrait ignores width sizing entirely (matches iOS/Android:
778
+ // both apply `maxContentWidth` only when not on a portrait phone).
779
+ // `detached` + `detachedOffset` are still respected via the wrapper.
652
780
  const maxWidth = isLandscapeOrTablet
653
- ? isFormSheet
781
+ ? presentation === 'form'
654
782
  ? DEFAULT_FORM_SHEET_WIDTH
655
- : (maxContentWidth ?? (pageSizing ? DEFAULT_MAX_WIDTH : undefined))
783
+ : (maxContentWidth ?? DEFAULT_MAX_WIDTH)
656
784
  : undefined;
657
785
 
658
- if (maxWidth == null && !effectiveDetached) return undefined;
786
+ const needsMargins = maxWidth != null || effectiveDetached;
787
+ if (!needsMargins && !dropShadow) return undefined;
788
+
789
+ const next: React.CSSProperties = {};
790
+ if (dropShadow) next.filter = dropShadow;
791
+ if (!needsMargins) return next;
659
792
 
660
793
  let marginLeft: number | string;
661
794
  let marginRight: number | string;
@@ -670,29 +803,40 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
670
803
  marginRight = anchor === 'right' ? anchorOffset : 'auto';
671
804
  }
672
805
 
673
- return {
674
- ...(maxWidth != null && { maxWidth }),
675
- marginLeft,
676
- marginRight,
677
- };
806
+ if (maxWidth != null) next.maxWidth = maxWidth;
807
+ next.marginLeft = marginLeft;
808
+ next.marginRight = marginRight;
809
+ return next;
678
810
  }, [
679
811
  isLandscapeOrTablet,
680
812
  isFormSheet,
681
813
  maxContentWidth,
682
- pageSizing,
814
+ presentation,
683
815
  anchor,
684
816
  anchorOffset,
685
817
  effectiveDetached,
818
+ dropShadow,
686
819
  ]);
687
820
 
821
+ // Absolute-position the grabber so it overlays the content top-edge
822
+ // instead of consuming flow height — mirrors native iOS/Android, where
823
+ // the grabber sits in the rounded corner zone above the content and
824
+ // doesn't push the header down or inflate the 'auto' detent measurement.
688
825
  const handleStyle = useMemo<React.CSSProperties>(
689
826
  () => ({
827
+ position: 'absolute',
828
+ top: grabberOptions?.topMargin ?? DEFAULT_GRABBER_TOP_MARGIN,
829
+ left: '50%',
830
+ transform: 'translateX(-50%)',
690
831
  height: grabberHeight,
691
832
  width: grabberOptions?.width ?? DEFAULT_GRABBER_WIDTH,
692
833
  borderRadius: grabberOptions?.cornerRadius ?? grabberHeight / 2,
693
834
  backgroundColor: (grabberOptions?.color ?? defaultGrabberColor) as string,
694
835
  opacity: 1,
695
- marginTop: grabberOptions?.topMargin ?? DEFAULT_GRABBER_TOP_MARGIN,
836
+ // Above absolute-positioned headers (which often use zIndex:1 to overlay
837
+ // scroll content) so the grabber stays draggable — critical when
838
+ // `handleOnly` mode means only the grabber can drag the sheet.
839
+ zIndex: 2,
696
840
  }),
697
841
  [grabberOptions, grabberHeight, defaultGrabberColor]
698
842
  );
@@ -706,7 +850,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
706
850
  onRelease={handleRelease}
707
851
  dismissible={dismissible}
708
852
  draggable={draggable}
709
- handleOnly={scrollable && scrollableOptions?.scrollingExpandsSheet === false}
710
853
  repositionInputs={false}
711
854
  modal={dimmed}
712
855
  nested={isNested}
@@ -716,6 +859,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
716
859
  maxContentHeight={effectiveMaxContentHeight}
717
860
  initialAnimated={initialDetentAnimated}
718
861
  detachedWrapperStyle={wrapperStyle}
862
+ onContentHeightChange={setMeasuredContentHeight}
719
863
  activeSnapPoint={activeSnapPoint}
720
864
  setActiveSnapPoint={handleSetActiveSnapPoint}
721
865
  {...snapPointsProps}
@@ -738,17 +882,31 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
738
882
  >
739
883
  <Drawer.Title style={visuallyHiddenStyle}>Sheet</Drawer.Title>
740
884
  {grabber && <Drawer.Handle style={handleStyle} />}
741
- {header && (
742
- <View style={headerStyle}>
743
- {isValidElement(header) ? header : createElement(header)}
744
- </View>
745
- )}
746
885
  {scrollable ? (
747
- <div style={scrollableContainerStyle}>
748
- <View style={style}>{children}</View>
886
+ // vaul wraps children in `[data-vaul-auto-size-wrapper]` (display:
887
+ // flow-root) which doesn't honor descendant flex layout. Use an
888
+ // absolute fill sized to the visible portion (via vaul's
889
+ // `--snap-point-height` var) so the inner flex column has a
890
+ // definite height for the scroll container's flex:1 to fill.
891
+ <div style={scrollableLayoutStyle}>
892
+ {header && (
893
+ <View style={headerStyle}>
894
+ {isValidElement(header) ? header : createElement(header)}
895
+ </View>
896
+ )}
897
+ <div style={scrollableContainerStyle}>
898
+ <View style={style}>{children}</View>
899
+ </div>
749
900
  </div>
750
901
  ) : (
751
- <View style={style}>{children}</View>
902
+ <>
903
+ {header && (
904
+ <View style={headerStyle}>
905
+ {isValidElement(header) ? header : createElement(header)}
906
+ </View>
907
+ )}
908
+ <View style={style}>{children}</View>
909
+ </>
752
910
  )}
753
911
  </Drawer.Content>
754
912
  </Drawer.Portal>
@@ -762,6 +920,16 @@ const overlayStyle: React.CSSProperties = {
762
920
  backgroundColor: 'rgba(0, 0, 0, 0.5)',
763
921
  };
764
922
 
923
+ const scrollableLayoutStyle: React.CSSProperties = {
924
+ position: 'absolute',
925
+ top: 0,
926
+ left: 0,
927
+ right: 0,
928
+ height: 'calc(100% - var(--snap-point-height, 0px))',
929
+ display: 'flex',
930
+ flexDirection: 'column',
931
+ };
932
+
765
933
  const scrollableContainerStyle: React.CSSProperties = {
766
934
  flex: 1,
767
935
  minHeight: 0,
@@ -19,6 +19,7 @@ type NodeRef = RefObject<HTMLDivElement | null>;
19
19
  interface StackEntry {
20
20
  ref: SheetRef;
21
21
  nodeRef: NodeRef;
22
+ isFormSheetRef: RefObject<boolean>;
22
23
  }
23
24
 
24
25
  interface SheetContextValue {
@@ -166,10 +167,18 @@ export function useRegisterSheet(name: string | undefined, ref: SheetRef): void
166
167
  * Registers the sheet in the open stack while `isOpen` is true and returns
167
168
  * live data used by each sheet to render stacked visuals and dismiss children.
168
169
  */
169
- export function useSheetStack(ref: SheetRef, nodeRef: NodeRef, isOpen: boolean) {
170
+ export function useSheetStack(
171
+ ref: SheetRef,
172
+ nodeRef: NodeRef,
173
+ isOpen: boolean,
174
+ isFormSheet: boolean
175
+ ) {
170
176
  const ctx = useContext(SheetContext);
171
177
 
172
- const entry = useMemo<StackEntry>(() => ({ ref, nodeRef }), [ref, nodeRef]);
178
+ const isFormSheetRef = useRef(isFormSheet);
179
+ isFormSheetRef.current = isFormSheet;
180
+
181
+ const entry = useMemo<StackEntry>(() => ({ ref, nodeRef, isFormSheetRef }), [ref, nodeRef]);
173
182
 
174
183
  useEffect(() => {
175
184
  if (!ctx || !isOpen) return;
@@ -101,7 +101,7 @@ export interface NativeProps extends ViewProps {
101
101
  initialDetentAnimated?: WithDefault<boolean, true>;
102
102
  scrollable?: WithDefault<boolean, false>;
103
103
  scrollableOptions?: ScrollableOptionsType;
104
- pageSizing?: WithDefault<boolean, true>;
104
+ presentation?: WithDefault<'page' | 'form', 'page'>;
105
105
 
106
106
  // Event handlers
107
107
  onMount?: DirectEventHandler<null>;
@@ -136,7 +136,7 @@ export type TrueSheetNavigationSheetProps = Pick<
136
136
  | 'maxContentHeight'
137
137
  | 'maxContentWidth'
138
138
  | 'scrollable'
139
- | 'pageSizing'
139
+ | 'presentation'
140
140
  | 'header'
141
141
  | 'headerStyle'
142
142
  | 'footer'
@@ -10,4 +10,4 @@ export const DEFAULT_GRABBER_COLOR_LIGHT = 'rgba(0, 0, 0, 0.3)';
10
10
  export const DEFAULT_GRABBER_COLOR_DARK = 'rgba(255, 255, 255, 0.3)';
11
11
  export const DEFAULT_GRABBER_WIDTH = 32;
12
12
  export const DEFAULT_GRABBER_HEIGHT = 4;
13
- export const DEFAULT_GRABBER_TOP_MARGIN = 16;
13
+ export const DEFAULT_GRABBER_TOP_MARGIN = 8;