@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.
- package/LICENSE +1 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +3 -3
- package/ios/TrueSheetFooterView.mm +10 -1
- package/ios/TrueSheetView.mm +1 -1
- package/ios/TrueSheetViewController.h +1 -1
- package/ios/TrueSheetViewController.mm +10 -4
- package/lib/module/TrueSheet.js +2 -2
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +222 -50
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/TrueSheetProvider.web.js +5 -2
- package/lib/module/TrueSheetProvider.web.js.map +1 -1
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +1 -1
- package/lib/module/web/constants.js +1 -1
- package/lib/module/web/constants.js.map +1 -1
- package/lib/module/web/vaul/index.js +88 -133
- package/lib/module/web/vaul/index.js.map +1 -1
- package/lib/module/web/vaul/style.css +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +6 -4
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheetProvider.web.d.ts +2 -1
- package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/navigation/types.d.ts +1 -1
- package/lib/typescript/src/navigation/types.d.ts.map +1 -1
- package/lib/typescript/src/web/constants.d.ts +1 -1
- package/lib/typescript/src/web/constants.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/index.d.ts +7 -1
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/TrueSheet.tsx +2 -2
- package/src/TrueSheet.types.ts +6 -4
- package/src/TrueSheet.web.tsx +220 -52
- package/src/TrueSheetProvider.web.tsx +11 -2
- package/src/fabric/TrueSheetViewNativeComponent.ts +1 -1
- package/src/navigation/types.ts +1 -1
- package/src/web/constants.ts +1 -1
- package/src/web/vaul/index.tsx +118 -146
- package/src/web/vaul/style.css +1 -1
package/src/TrueSheet.web.tsx
CHANGED
|
@@ -76,8 +76,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
76
76
|
footer,
|
|
77
77
|
footerStyle,
|
|
78
78
|
scrollable = false,
|
|
79
|
-
|
|
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
|
-
//
|
|
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 =
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
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
|
-
|
|
587
|
-
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
630
|
-
// default width and a height
|
|
631
|
-
//
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
-
//
|
|
647
|
-
//
|
|
648
|
-
//
|
|
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
|
-
?
|
|
781
|
+
? presentation === 'form'
|
|
654
782
|
? DEFAULT_FORM_SHEET_WIDTH
|
|
655
|
-
: (maxContentWidth ??
|
|
783
|
+
: (maxContentWidth ?? DEFAULT_MAX_WIDTH)
|
|
656
784
|
: undefined;
|
|
657
785
|
|
|
658
|
-
|
|
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
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
748
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
104
|
+
presentation?: WithDefault<'page' | 'form', 'page'>;
|
|
105
105
|
|
|
106
106
|
// Event handlers
|
|
107
107
|
onMount?: DirectEventHandler<null>;
|
package/src/navigation/types.ts
CHANGED
package/src/web/constants.ts
CHANGED
|
@@ -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 =
|
|
13
|
+
export const DEFAULT_GRABBER_TOP_MARGIN = 8;
|