@lodev09/react-native-true-sheet 3.11.0-beta.0 → 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 +226 -51
- 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 +102 -127
- 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 +8 -2
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/use-prevent-scroll.d.ts +2 -2
- package/lib/typescript/src/web/vaul/use-prevent-scroll.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 +225 -55
- 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 +550 -552
- 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,6 +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
|
+
|
|
120
|
+
// presentation='form' implies a floating/detached sheet on web — mirrors iOS
|
|
121
|
+
// form-sheet semantics where the sheet is never edge-attached.
|
|
122
|
+
const effectiveDetached = presentation === 'form' || detached;
|
|
119
123
|
|
|
120
124
|
const colorScheme = useColorScheme();
|
|
121
125
|
const backgroundColor =
|
|
@@ -167,6 +171,17 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
167
171
|
// in another screen's tree when navigating) should not close the drawer.
|
|
168
172
|
if (portalContainer && !portalContainer.contains(target)) {
|
|
169
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
|
+
}
|
|
170
185
|
}
|
|
171
186
|
};
|
|
172
187
|
|
|
@@ -260,7 +275,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
260
275
|
if (count === 0) return { index: -1, detent: 0 };
|
|
261
276
|
|
|
262
277
|
const windowH = window.innerHeight;
|
|
263
|
-
const effectiveH =
|
|
278
|
+
const effectiveH = effectiveDetached ? windowH - detachedOffset : windowH;
|
|
264
279
|
// Matches vaul's height ceiling: min(effectiveH, maxContentHeight).
|
|
265
280
|
const ceiling =
|
|
266
281
|
maxContentHeight !== undefined ? Math.min(effectiveH, maxContentHeight) : effectiveH;
|
|
@@ -329,7 +344,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
329
344
|
|
|
330
345
|
return { index: count - 1, detent: values[count - 1]! };
|
|
331
346
|
},
|
|
332
|
-
[
|
|
347
|
+
[effectiveDetached, detachedOffset, maxContentHeight]
|
|
333
348
|
);
|
|
334
349
|
|
|
335
350
|
const handlePositionChange = useCallback(
|
|
@@ -366,8 +381,15 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
366
381
|
|
|
367
382
|
const present = !wasOpen && isOpen;
|
|
368
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.
|
|
369
387
|
onWillPresentRef.current?.({ nativeEvent: computeDetentInfo() } as WillPresentEvent);
|
|
388
|
+
onWillFocusRef.current?.({ nativeEvent: null } as WillFocusEvent);
|
|
370
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);
|
|
371
393
|
onWillDismissRef.current?.({ nativeEvent: null } as WillDismissEvent);
|
|
372
394
|
} else {
|
|
373
395
|
return undefined;
|
|
@@ -376,7 +398,9 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
376
398
|
const fireDone = () => {
|
|
377
399
|
if (present) {
|
|
378
400
|
onDidPresentRef.current?.({ nativeEvent: computeDetentInfo() } as DidPresentEvent);
|
|
401
|
+
onDidFocusRef.current?.({ nativeEvent: null } as DidFocusEvent);
|
|
379
402
|
} else {
|
|
403
|
+
onDidBlurRef.current?.({ nativeEvent: null } as DidBlurEvent);
|
|
380
404
|
onDidDismissRef.current?.({ nativeEvent: null } as DidDismissEvent);
|
|
381
405
|
}
|
|
382
406
|
};
|
|
@@ -459,7 +483,8 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
459
483
|
const { isNested, dismissAbove, descendants } = useSheetStack(
|
|
460
484
|
methodsRef,
|
|
461
485
|
drawerContentRef,
|
|
462
|
-
isOpen
|
|
486
|
+
isOpen,
|
|
487
|
+
isFormSheet
|
|
463
488
|
);
|
|
464
489
|
dismissAboveRef.current = dismissAbove;
|
|
465
490
|
|
|
@@ -469,28 +494,97 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
469
494
|
useEffect(() => {
|
|
470
495
|
const parent = drawerContentRef.current;
|
|
471
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]');
|
|
472
504
|
|
|
473
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
|
+
};
|
|
474
527
|
|
|
475
528
|
if (descendants.length === 0) {
|
|
476
529
|
parent.style.transition = transition;
|
|
477
530
|
parent.style.transform = '';
|
|
531
|
+
setClip(CLIP_NONE);
|
|
478
532
|
return;
|
|
479
533
|
}
|
|
480
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.
|
|
481
539
|
const computeTargetY = () => {
|
|
482
540
|
const parentSnap = parseFloat(parent.style.getPropertyValue('--snap-point-height')) || 0;
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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;
|
|
489
558
|
}
|
|
490
|
-
|
|
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)`);
|
|
491
575
|
};
|
|
492
576
|
|
|
493
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
|
+
}
|
|
494
588
|
const targetY = computeTargetY();
|
|
495
589
|
const match = parent.style.transform.match(/translate3d\([^,]*,\s*(-?\d*\.?\d+)px/);
|
|
496
590
|
const currentY = match ? parseFloat(match[1]!) : 0;
|
|
@@ -510,7 +604,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
510
604
|
cancelAnimationFrame(raf);
|
|
511
605
|
observer.disconnect();
|
|
512
606
|
};
|
|
513
|
-
}, [descendants, activeSnapPoint]);
|
|
607
|
+
}, [descendants, activeSnapPoint, cornerRadius, isFormSheet, isOpen]);
|
|
514
608
|
|
|
515
609
|
// Focus/blur events fire when a descendant sheet appears on top of this one
|
|
516
610
|
// (blur) or when all descendants are dismissed (focus). will-events fire
|
|
@@ -579,8 +673,17 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
579
673
|
// Shadow cast upward from the sheet's top edge toward the background. Matches
|
|
580
674
|
// Android's `elevation` semantics roughly — the sheet "lifts" off whatever is
|
|
581
675
|
// behind it. Scales linearly so higher elevation reads as more separation.
|
|
582
|
-
|
|
583
|
-
|
|
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;
|
|
584
687
|
|
|
585
688
|
const mergedContentStyle = useMemo<React.CSSProperties>(
|
|
586
689
|
() => ({
|
|
@@ -594,11 +697,13 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
594
697
|
borderTopLeftRadius: effectiveCornerRadius,
|
|
595
698
|
borderTopRightRadius: effectiveCornerRadius,
|
|
596
699
|
backgroundColor: backgroundColor as string,
|
|
597
|
-
|
|
700
|
+
// Clip children to the rounded top so headers/content with their own
|
|
701
|
+
// background don't bleed past the corners.
|
|
702
|
+
overflow: 'hidden',
|
|
598
703
|
// Lift content above iOS home indicator / bottom safe area when enabled.
|
|
599
704
|
paddingBottom: insetAdjustment === 'automatic' ? 'env(safe-area-inset-bottom, 0px)' : 0,
|
|
600
705
|
}),
|
|
601
|
-
[backgroundColor, effectiveCornerRadius,
|
|
706
|
+
[backgroundColor, effectiveCornerRadius, insetAdjustment]
|
|
602
707
|
);
|
|
603
708
|
|
|
604
709
|
const defaultGrabberColor =
|
|
@@ -622,38 +727,68 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
622
727
|
[]
|
|
623
728
|
);
|
|
624
729
|
|
|
625
|
-
// Form-sheet style (
|
|
626
|
-
// default width and a height
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
//
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
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]);
|
|
640
766
|
|
|
641
767
|
// The wrapper holds all horizontal sizing/anchoring so its rounded-bottom
|
|
642
768
|
// clip (when detached) aligns with the drawer's horizontal bounds on
|
|
643
769
|
// desktop — otherwise its corners sit at the far viewport edges.
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
//
|
|
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).
|
|
647
774
|
// Detached without a width constraint applies anchorOffset on both edges so
|
|
648
775
|
// the floating card breathes from the viewport sides.
|
|
649
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.
|
|
650
780
|
const maxWidth = isLandscapeOrTablet
|
|
651
|
-
?
|
|
781
|
+
? presentation === 'form'
|
|
652
782
|
? DEFAULT_FORM_SHEET_WIDTH
|
|
653
|
-
: (maxContentWidth ??
|
|
783
|
+
: (maxContentWidth ?? DEFAULT_MAX_WIDTH)
|
|
654
784
|
: undefined;
|
|
655
785
|
|
|
656
|
-
|
|
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;
|
|
657
792
|
|
|
658
793
|
let marginLeft: number | string;
|
|
659
794
|
let marginRight: number | string;
|
|
@@ -668,29 +803,40 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
668
803
|
marginRight = anchor === 'right' ? anchorOffset : 'auto';
|
|
669
804
|
}
|
|
670
805
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
};
|
|
806
|
+
if (maxWidth != null) next.maxWidth = maxWidth;
|
|
807
|
+
next.marginLeft = marginLeft;
|
|
808
|
+
next.marginRight = marginRight;
|
|
809
|
+
return next;
|
|
676
810
|
}, [
|
|
677
811
|
isLandscapeOrTablet,
|
|
678
812
|
isFormSheet,
|
|
679
813
|
maxContentWidth,
|
|
680
|
-
|
|
814
|
+
presentation,
|
|
681
815
|
anchor,
|
|
682
816
|
anchorOffset,
|
|
683
|
-
|
|
817
|
+
effectiveDetached,
|
|
818
|
+
dropShadow,
|
|
684
819
|
]);
|
|
685
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.
|
|
686
825
|
const handleStyle = useMemo<React.CSSProperties>(
|
|
687
826
|
() => ({
|
|
827
|
+
position: 'absolute',
|
|
828
|
+
top: grabberOptions?.topMargin ?? DEFAULT_GRABBER_TOP_MARGIN,
|
|
829
|
+
left: '50%',
|
|
830
|
+
transform: 'translateX(-50%)',
|
|
688
831
|
height: grabberHeight,
|
|
689
832
|
width: grabberOptions?.width ?? DEFAULT_GRABBER_WIDTH,
|
|
690
833
|
borderRadius: grabberOptions?.cornerRadius ?? grabberHeight / 2,
|
|
691
834
|
backgroundColor: (grabberOptions?.color ?? defaultGrabberColor) as string,
|
|
692
835
|
opacity: 1,
|
|
693
|
-
|
|
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,
|
|
694
840
|
}),
|
|
695
841
|
[grabberOptions, grabberHeight, defaultGrabberColor]
|
|
696
842
|
);
|
|
@@ -704,7 +850,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
704
850
|
onRelease={handleRelease}
|
|
705
851
|
dismissible={dismissible}
|
|
706
852
|
draggable={draggable}
|
|
707
|
-
handleOnly={scrollable && scrollableOptions?.scrollingExpandsSheet === false}
|
|
708
853
|
repositionInputs={false}
|
|
709
854
|
modal={dimmed}
|
|
710
855
|
nested={isNested}
|
|
@@ -714,6 +859,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
714
859
|
maxContentHeight={effectiveMaxContentHeight}
|
|
715
860
|
initialAnimated={initialDetentAnimated}
|
|
716
861
|
detachedWrapperStyle={wrapperStyle}
|
|
862
|
+
onContentHeightChange={setMeasuredContentHeight}
|
|
717
863
|
activeSnapPoint={activeSnapPoint}
|
|
718
864
|
setActiveSnapPoint={handleSetActiveSnapPoint}
|
|
719
865
|
{...snapPointsProps}
|
|
@@ -736,17 +882,31 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
736
882
|
>
|
|
737
883
|
<Drawer.Title style={visuallyHiddenStyle}>Sheet</Drawer.Title>
|
|
738
884
|
{grabber && <Drawer.Handle style={handleStyle} />}
|
|
739
|
-
{header && (
|
|
740
|
-
<View style={headerStyle}>
|
|
741
|
-
{isValidElement(header) ? header : createElement(header)}
|
|
742
|
-
</View>
|
|
743
|
-
)}
|
|
744
885
|
{scrollable ? (
|
|
745
|
-
|
|
746
|
-
|
|
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>
|
|
747
900
|
</div>
|
|
748
901
|
) : (
|
|
749
|
-
|
|
902
|
+
<>
|
|
903
|
+
{header && (
|
|
904
|
+
<View style={headerStyle}>
|
|
905
|
+
{isValidElement(header) ? header : createElement(header)}
|
|
906
|
+
</View>
|
|
907
|
+
)}
|
|
908
|
+
<View style={style}>{children}</View>
|
|
909
|
+
</>
|
|
750
910
|
)}
|
|
751
911
|
</Drawer.Content>
|
|
752
912
|
</Drawer.Portal>
|
|
@@ -760,6 +920,16 @@ const overlayStyle: React.CSSProperties = {
|
|
|
760
920
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
761
921
|
};
|
|
762
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
|
+
|
|
763
933
|
const scrollableContainerStyle: React.CSSProperties = {
|
|
764
934
|
flex: 1,
|
|
765
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;
|