@lodev09/react-native-true-sheet 4.0.0-beta.15 → 4.0.0-beta.17
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/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +0 -5
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +5 -95
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +11 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +18 -46
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +1 -1
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +3 -0
- package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +0 -12
- package/ios/TrueSheetFooterView.mm +27 -11
- package/ios/TrueSheetView.mm +1 -0
- package/lib/module/TrueSheet.web.js +49 -277
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/web/layout.js +168 -0
- package/lib/module/web/layout.js.map +1 -0
- package/lib/module/web/vaul/helpers.js +1 -0
- package/lib/module/web/vaul/helpers.js.map +1 -1
- package/lib/module/web/vaul/index.js +9 -4
- package/lib/module/web/vaul/index.js.map +1 -1
- package/lib/module/web/vaul/use-snap-points.js +6 -8
- package/lib/module/web/vaul/use-snap-points.js.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
- package/lib/typescript/src/web/layout.d.ts +19 -0
- package/lib/typescript/src/web/layout.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/helpers.d.ts +1 -0
- package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/TrueSheet.web.tsx +78 -333
- package/src/web/layout.ts +196 -0
- package/src/web/vaul/helpers.ts +5 -0
- package/src/web/vaul/index.tsx +21 -4
- package/src/web/vaul/use-snap-points.ts +6 -14
package/src/TrueSheet.web.tsx
CHANGED
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
DEFAULT_MAX_WIDTH,
|
|
58
58
|
} from './web/constants';
|
|
59
59
|
import { getDOMElement } from './web/dom';
|
|
60
|
+
import { observeSheetLayout } from './web/layout';
|
|
60
61
|
import { Drawer } from './web/vaul';
|
|
61
62
|
import { DEFAULT_PEEK_HEIGHT, DRAG_CLASS, TRANSITIONS } from './web/vaul/constants';
|
|
62
63
|
|
|
@@ -172,8 +173,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
172
173
|
const validDetentsRef = useRef(validDetents);
|
|
173
174
|
validDetentsRef.current = validDetents;
|
|
174
175
|
|
|
175
|
-
const hasAutoDetent = validDetents.includes('auto');
|
|
176
|
-
|
|
177
176
|
const handleSetActiveSnapPoint = useCallback((snapPoint: number | string | null) => {
|
|
178
177
|
setActiveSnapPoint(
|
|
179
178
|
snapPoint == null
|
|
@@ -334,9 +333,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
334
333
|
// An absolute (floating) header overlaps the content, so it contributes no
|
|
335
334
|
// height to the 'auto' detent measurement.
|
|
336
335
|
const absoluteHeader = headerOptions?.position === 'absolute';
|
|
337
|
-
const absoluteHeaderRef = useRef(absoluteHeader);
|
|
338
|
-
absoluteHeaderRef.current = absoluteHeader;
|
|
339
|
-
|
|
340
336
|
const resolvedHeaderStyle = absoluteHeader ? [absoluteHeaderStyle, headerStyle] : headerStyle;
|
|
341
337
|
|
|
342
338
|
// Same for an absolute (floating) footer — rendered via vaul's
|
|
@@ -372,113 +368,48 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
372
368
|
(footer && absoluteFooter ? footerHeight : 0) +
|
|
373
369
|
peekContentHeight || DEFAULT_PEEK_HEIGHT;
|
|
374
370
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
// Bottom padding applied to the plugged scrollable for an absolute footer
|
|
384
|
-
// (see the footer inset effect below)
|
|
385
|
-
const footerInsetRef = useRef(0);
|
|
386
|
-
|
|
387
|
-
// Natural content height (header + content + footer, with a detected
|
|
388
|
-
// scrollable's viewport replaced by its content size) — the height the
|
|
389
|
-
// content wants regardless of the sheet's bounds. An absolute footer's inset
|
|
390
|
-
// on the scrollable counts like a relative footer's height, so the content
|
|
391
|
-
// ends above the footer (mirrors native's auto detent).
|
|
392
|
-
const measureNaturalHeight = useCallback(() => {
|
|
393
|
-
const contentEl = getDOMElement(contentRef.current);
|
|
394
|
-
if (!contentEl || !contentEl.isConnected) return 0;
|
|
395
|
-
const headerEl = absoluteHeaderRef.current ? null : getDOMElement(headerElRef.current);
|
|
396
|
-
const footerEl = absoluteFooterRef.current ? null : getDOMElement(footerElRef.current);
|
|
397
|
-
let height =
|
|
398
|
-
(headerEl?.offsetHeight ?? 0) + (footerEl?.offsetHeight ?? 0) + contentEl.offsetHeight;
|
|
399
|
-
const scroller = detectedScrollerRef.current;
|
|
400
|
-
const scrollContent = scroller?.firstElementChild;
|
|
401
|
-
if (scroller?.isConnected && scrollContent instanceof HTMLElement) {
|
|
402
|
-
height += scrollContent.offsetHeight - scroller.clientHeight + footerInsetRef.current;
|
|
403
|
-
}
|
|
404
|
-
return Math.max(0, height);
|
|
405
|
-
}, []);
|
|
371
|
+
const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
|
|
372
|
+
const naturalHeightRef = useRef(0);
|
|
373
|
+
const insetBehavior = scrollableOptions?.contentInsetAdjustment ?? 'automatic';
|
|
374
|
+
const footerInsetAdjustment =
|
|
375
|
+
Boolean(footer) &&
|
|
376
|
+
absoluteFooter &&
|
|
377
|
+
(insetBehavior === 'automatic' || insetBehavior === 'footer');
|
|
378
|
+
const measureContent = validDetents.includes('auto') || isFormSheet;
|
|
406
379
|
|
|
407
380
|
useEffect(() => {
|
|
408
|
-
if (!isOpen
|
|
381
|
+
if (!isOpen) return undefined;
|
|
409
382
|
|
|
410
383
|
let canceled = false;
|
|
411
384
|
let rafId = 0;
|
|
412
|
-
let
|
|
413
|
-
let resizeObserver: ResizeObserver | null = null;
|
|
385
|
+
let observer: ReturnType<typeof observeSheetLayout> | undefined;
|
|
414
386
|
|
|
415
387
|
const attach = () => {
|
|
416
388
|
if (canceled) return;
|
|
417
|
-
const
|
|
418
|
-
|
|
389
|
+
const layoutEl = sizedLayoutRef.current;
|
|
390
|
+
const drawer = drawerContentRef.current;
|
|
391
|
+
if (!drawer || !layoutEl?.isConnected) {
|
|
419
392
|
// Radix Presence defers the portal mount; poll until the drawer is live.
|
|
420
393
|
rafId = window.requestAnimationFrame(attach);
|
|
421
394
|
return;
|
|
422
395
|
}
|
|
423
396
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
const headerEl = getDOMElement(headerElRef.current);
|
|
441
|
-
|
|
442
|
-
// Skip mutations that don't change the observed topology — e.g. a
|
|
443
|
-
// virtualized list mounting rows inside the plugged scroller. Size
|
|
444
|
-
// changes are already covered by the ResizeObserver.
|
|
445
|
-
if (
|
|
446
|
-
observed &&
|
|
447
|
-
observed.contentEl === contentEl &&
|
|
448
|
-
contentEl?.isConnected &&
|
|
449
|
-
observed.headerEl === headerEl &&
|
|
450
|
-
observed.scroller.isConnected &&
|
|
451
|
-
observed.scrollContent === observed.scroller.firstElementChild
|
|
452
|
-
) {
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const resolved =
|
|
457
|
-
contentEl && contentEl.isConnected ? getScrollableElement(scrollableRef?.current) : null;
|
|
458
|
-
const scroller = resolved?.isConnected ? resolved : null;
|
|
459
|
-
detectedScrollerRef.current = scroller;
|
|
460
|
-
setHasBoundedScrollable(scroller != null);
|
|
461
|
-
observed = scroller
|
|
462
|
-
? { contentEl, headerEl, scroller, scrollContent: scroller.firstElementChild }
|
|
463
|
-
: null;
|
|
464
|
-
|
|
465
|
-
resizeObserver?.disconnect();
|
|
466
|
-
resizeObserver = new ResizeObserver(measure);
|
|
467
|
-
if (contentEl?.isConnected) resizeObserver.observe(contentEl);
|
|
468
|
-
if (headerEl) resizeObserver.observe(headerEl);
|
|
469
|
-
if (scroller) {
|
|
470
|
-
resizeObserver.observe(scroller);
|
|
471
|
-
if (scroller.firstElementChild) resizeObserver.observe(scroller.firstElementChild);
|
|
472
|
-
}
|
|
473
|
-
measure();
|
|
474
|
-
};
|
|
475
|
-
|
|
476
|
-
observe();
|
|
477
|
-
// Watch the whole drawer subtree — catches the scrollable mounting/
|
|
478
|
-
// unmounting AND the layout-branch swap, which remounts the content node
|
|
479
|
-
// (a content-scoped observer would go stale after the swap).
|
|
480
|
-
mutationObserver = new MutationObserver(observe);
|
|
481
|
-
mutationObserver.observe(drawerEl, { childList: true, subtree: true });
|
|
397
|
+
observer = observeSheetLayout({
|
|
398
|
+
drawer,
|
|
399
|
+
layout: layoutEl,
|
|
400
|
+
getContent: () => getDOMElement(contentRef.current),
|
|
401
|
+
getHeader: () => getDOMElement(headerElRef.current),
|
|
402
|
+
getFooter: () => getDOMElement(footerElRef.current),
|
|
403
|
+
getScrollable: () => getScrollableElement(scrollableRef?.current),
|
|
404
|
+
absoluteHeader,
|
|
405
|
+
absoluteFooter,
|
|
406
|
+
footerInsetAdjustment,
|
|
407
|
+
measureContent,
|
|
408
|
+
onHeightChange: (height) => {
|
|
409
|
+
naturalHeightRef.current = height;
|
|
410
|
+
setMeasuredContentHeight(height);
|
|
411
|
+
},
|
|
412
|
+
});
|
|
482
413
|
};
|
|
483
414
|
|
|
484
415
|
rafId = window.requestAnimationFrame(attach);
|
|
@@ -486,10 +417,16 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
486
417
|
return () => {
|
|
487
418
|
canceled = true;
|
|
488
419
|
window.cancelAnimationFrame(rafId);
|
|
489
|
-
|
|
490
|
-
resizeObserver?.disconnect();
|
|
420
|
+
observer?.disconnect();
|
|
491
421
|
};
|
|
492
|
-
}, [
|
|
422
|
+
}, [
|
|
423
|
+
isOpen,
|
|
424
|
+
absoluteHeader,
|
|
425
|
+
absoluteFooter,
|
|
426
|
+
footerInsetAdjustment,
|
|
427
|
+
measureContent,
|
|
428
|
+
scrollableRef,
|
|
429
|
+
]);
|
|
493
430
|
|
|
494
431
|
// Below the last detent a vertical touch pan moves the sheet, not the
|
|
495
432
|
// content — `[data-vaul-scroll-locked]` disables vertical touch panning on
|
|
@@ -497,13 +434,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
497
434
|
const isScrollLocked =
|
|
498
435
|
validDetents.length > 0 && activeSnapPoint !== validDetents[validDetents.length - 1];
|
|
499
436
|
|
|
500
|
-
// Vaul measures the auto-size wrapper's offsetHeight (always, post fork).
|
|
501
|
-
// Track it here so the form sheet can size its card to fit content,
|
|
502
|
-
// clamped between a minimum ratio of the viewport and a maximum derived
|
|
503
|
-
// from `detachedOffset` (the breathing room left at top + bottom of the
|
|
504
|
-
// floating card).
|
|
505
|
-
const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
|
|
506
|
-
|
|
507
437
|
const effectiveMaxContentHeight = useMemo<number | undefined>(() => {
|
|
508
438
|
if (maxContentHeight !== undefined) return maxContentHeight;
|
|
509
439
|
if (!isFormSheet) return undefined;
|
|
@@ -570,7 +500,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
570
500
|
effectiveMaxContentHeight,
|
|
571
501
|
peekHeight,
|
|
572
502
|
peekContentHeight,
|
|
573
|
-
measuredContentHeight,
|
|
574
503
|
});
|
|
575
504
|
geometryInputsRef.current = {
|
|
576
505
|
effectiveDetached,
|
|
@@ -578,7 +507,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
578
507
|
effectiveMaxContentHeight,
|
|
579
508
|
peekHeight,
|
|
580
509
|
peekContentHeight,
|
|
581
|
-
measuredContentHeight,
|
|
582
510
|
};
|
|
583
511
|
|
|
584
512
|
const computeDetentGeometry = useCallback(() => {
|
|
@@ -592,12 +520,9 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
592
520
|
inputs.effectiveMaxContentHeight !== undefined
|
|
593
521
|
? Math.min(effectiveH, inputs.effectiveMaxContentHeight)
|
|
594
522
|
: effectiveH;
|
|
595
|
-
//
|
|
596
|
-
//
|
|
597
|
-
|
|
598
|
-
// so it reads 0 until the ResizeObserver fires). Falls back to the state
|
|
599
|
-
// value, then to vaul's pre-measure fallback (effectiveHeight / 2).
|
|
600
|
-
const measuredHeight = measureNaturalHeight() || inputs.measuredContentHeight;
|
|
523
|
+
// Reuse the intrinsic measurement during position events so animation
|
|
524
|
+
// frames don't clone the content tree again.
|
|
525
|
+
const measuredHeight = naturalHeightRef.current;
|
|
601
526
|
const autoHeight = Math.min(measuredHeight > 0 ? measuredHeight : effectiveH / 2, ceiling);
|
|
602
527
|
|
|
603
528
|
// 'peek' is measured live from the DOM (offsetHeight is layout-based, so
|
|
@@ -633,7 +558,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
633
558
|
values.push(effectiveH > 0 ? h / effectiveH : 0);
|
|
634
559
|
}
|
|
635
560
|
return { windowH, effectiveH, ceiling, positions, values };
|
|
636
|
-
}, [
|
|
561
|
+
}, []);
|
|
637
562
|
|
|
638
563
|
// Detent info for lifecycle events. Position/detent come from the active
|
|
639
564
|
// detent's target geometry — not the live DOM rect — so willPresent (drawer
|
|
@@ -712,149 +637,19 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
712
637
|
[computeDetentGeometry]
|
|
713
638
|
);
|
|
714
639
|
|
|
715
|
-
// Mirror native synchronous layout: while dragging (and through the post-
|
|
716
|
-
// release settle) the sized layout tracks the sheet's visible height in
|
|
717
|
-
// realtime, clamped to the smallest detent — dragging below it slides the
|
|
718
|
-
// sheet out without resizing. Once settled, the inline height is cleared so
|
|
719
|
-
// the CSS calc() owns sizing again (adapts to viewport resizes at rest).
|
|
720
640
|
const sizedLayoutRef = useRef<HTMLDivElement>(null);
|
|
721
641
|
const isDraggingRef = useRef(false);
|
|
722
|
-
const isSettlingAfterDragRef = useRef(false);
|
|
723
|
-
const updateSizedLayoutHeight = useCallback(
|
|
724
|
-
(position: number) => {
|
|
725
|
-
const node = sizedLayoutRef.current;
|
|
726
|
-
if (!node) return;
|
|
727
|
-
|
|
728
|
-
const { effectiveH, ceiling, positions } = computeDetentGeometry();
|
|
729
|
-
if (positions.length === 0) return;
|
|
730
|
-
|
|
731
|
-
if (isSettlingAfterDragRef.current) {
|
|
732
|
-
const snap = activeSnapPointRef.current;
|
|
733
|
-
const index = snap != null ? validDetentsRef.current.indexOf(snap) : -1;
|
|
734
|
-
const target = index >= 0 ? positions[index]! : null;
|
|
735
|
-
if (target != null && Math.abs(position - target) < 1) {
|
|
736
|
-
isSettlingAfterDragRef.current = false;
|
|
737
|
-
// Restore the calc — height is an inline style, so clearing it would
|
|
738
|
-
// leave the div unsized (React won't re-apply it without a render)
|
|
739
|
-
node.style.height = SIZED_LAYOUT_HEIGHT;
|
|
740
|
-
return;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// positions[0] is the smallest detent's top-Y — its height is the clamp floor.
|
|
745
|
-
const minHeight = effectiveH - positions[0]!;
|
|
746
|
-
const height = Math.min(Math.max(effectiveH - position, minHeight), ceiling);
|
|
747
|
-
node.style.height = `${height}px`;
|
|
748
|
-
},
|
|
749
|
-
[computeDetentGeometry]
|
|
750
|
-
);
|
|
751
642
|
|
|
752
643
|
const handlePositionChange = useCallback(
|
|
753
644
|
(position: number) => {
|
|
754
|
-
if (isDraggingRef.current || isSettlingAfterDragRef.current) {
|
|
755
|
-
updateSizedLayoutHeight(position);
|
|
756
|
-
}
|
|
757
645
|
const { index, detent } = interpolateFromPosition(position);
|
|
758
646
|
onPositionChangeRef.current?.({
|
|
759
647
|
nativeEvent: { index, position, detent, realtime: true },
|
|
760
648
|
} as PositionChangeEvent);
|
|
761
649
|
},
|
|
762
|
-
[interpolateFromPosition
|
|
650
|
+
[interpolateFromPosition]
|
|
763
651
|
);
|
|
764
652
|
|
|
765
|
-
// Web mirror of native's footer inset (`contentInsetAdjustment`
|
|
766
|
-
// 'automatic' | 'footer'): pad the plugged scrollable by how much the
|
|
767
|
-
// absolute footer covers it. Measured from layout offsets (not client rects)
|
|
768
|
-
// so in-flight transforms don't skew it — the footer's bottom sits at the
|
|
769
|
-
// visible bottom of the layout container, so only a scroller reaching that
|
|
770
|
-
// edge gets an inset, and a safe-area lift (or a short scroller) shrinks it.
|
|
771
|
-
// Padding on the scroll container extends its scrollable overflow, like a
|
|
772
|
-
// native content inset.
|
|
773
|
-
const insetBehavior = scrollableOptions?.contentInsetAdjustment ?? 'automatic';
|
|
774
|
-
const footerInsetAdjustment =
|
|
775
|
-
Boolean(footer) &&
|
|
776
|
-
absoluteFooter &&
|
|
777
|
-
(insetBehavior === 'automatic' || insetBehavior === 'footer');
|
|
778
|
-
|
|
779
|
-
useEffect(() => {
|
|
780
|
-
if (!isOpen || !footerInsetAdjustment || !scrollableRef) return undefined;
|
|
781
|
-
|
|
782
|
-
let canceled = false;
|
|
783
|
-
let rafId = 0;
|
|
784
|
-
let mutationObserver: MutationObserver | null = null;
|
|
785
|
-
let resizeObserver: ResizeObserver | null = null;
|
|
786
|
-
let scroller: HTMLElement | null = null;
|
|
787
|
-
|
|
788
|
-
const reset = () => {
|
|
789
|
-
if (scroller) scroller.style.paddingBottom = '';
|
|
790
|
-
footerInsetRef.current = 0;
|
|
791
|
-
};
|
|
792
|
-
|
|
793
|
-
const apply = () => {
|
|
794
|
-
const footerEl = getDOMElement(footerElRef.current);
|
|
795
|
-
const container = sizedLayoutRef.current ?? drawerContentRef.current;
|
|
796
|
-
if (!scroller?.isConnected || !footerEl || !container) return;
|
|
797
|
-
|
|
798
|
-
// Measure without our own padding so an unbounded scroller (sized by its
|
|
799
|
-
// content) doesn't grow by the inset and feed back into the overlap.
|
|
800
|
-
scroller.style.paddingBottom = '';
|
|
801
|
-
let bottom = scroller.offsetTop + scroller.offsetHeight;
|
|
802
|
-
let el = scroller.offsetParent as HTMLElement | null;
|
|
803
|
-
while (el && el !== container) {
|
|
804
|
-
bottom += el.offsetTop;
|
|
805
|
-
el = el.offsetParent as HTMLElement | null;
|
|
806
|
-
}
|
|
807
|
-
const footerTop = container.offsetHeight - footerEl.offsetHeight;
|
|
808
|
-
const inset = el ? Math.max(0, bottom - footerTop) : 0;
|
|
809
|
-
footerInsetRef.current = inset;
|
|
810
|
-
scroller.style.paddingBottom = inset > 0 ? `${inset}px` : '';
|
|
811
|
-
};
|
|
812
|
-
|
|
813
|
-
// (Re)resolve the plugged scrollable — a conditional remount swaps the node
|
|
814
|
-
const observe = () => {
|
|
815
|
-
const resolved = getScrollableElement(scrollableRef.current);
|
|
816
|
-
const next = resolved?.isConnected ? resolved : null;
|
|
817
|
-
if (next === scroller && resizeObserver) return;
|
|
818
|
-
|
|
819
|
-
reset();
|
|
820
|
-
resizeObserver?.disconnect();
|
|
821
|
-
resizeObserver = null;
|
|
822
|
-
scroller = next;
|
|
823
|
-
if (!scroller) return;
|
|
824
|
-
|
|
825
|
-
resizeObserver = new ResizeObserver(apply);
|
|
826
|
-
resizeObserver.observe(scroller);
|
|
827
|
-
const footerEl = getDOMElement(footerElRef.current);
|
|
828
|
-
if (footerEl) resizeObserver.observe(footerEl);
|
|
829
|
-
const container = sizedLayoutRef.current ?? drawerContentRef.current;
|
|
830
|
-
if (container) resizeObserver.observe(container);
|
|
831
|
-
apply();
|
|
832
|
-
};
|
|
833
|
-
|
|
834
|
-
const attach = () => {
|
|
835
|
-
if (canceled) return;
|
|
836
|
-
const drawerEl = drawerContentRef.current;
|
|
837
|
-
if (!drawerEl || !drawerEl.isConnected) {
|
|
838
|
-
// Radix Presence defers the portal mount; poll until the drawer is live.
|
|
839
|
-
rafId = window.requestAnimationFrame(attach);
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
842
|
-
observe();
|
|
843
|
-
mutationObserver = new MutationObserver(observe);
|
|
844
|
-
mutationObserver.observe(drawerEl, { childList: true, subtree: true });
|
|
845
|
-
};
|
|
846
|
-
|
|
847
|
-
rafId = window.requestAnimationFrame(attach);
|
|
848
|
-
|
|
849
|
-
return () => {
|
|
850
|
-
canceled = true;
|
|
851
|
-
window.cancelAnimationFrame(rafId);
|
|
852
|
-
mutationObserver?.disconnect();
|
|
853
|
-
resizeObserver?.disconnect();
|
|
854
|
-
reset();
|
|
855
|
-
};
|
|
856
|
-
}, [isOpen, footerInsetAdjustment, scrollableRef]);
|
|
857
|
-
|
|
858
653
|
// Fire onMount once after first render. React-mount is the earliest point
|
|
859
654
|
// the component is ready for imperative calls, matching the native
|
|
860
655
|
// "ready for present" contract. Declared before the present/dismiss effect
|
|
@@ -980,17 +775,11 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
980
775
|
}
|
|
981
776
|
onDragChangeRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragChangeEvent);
|
|
982
777
|
}, [computeDetentInfo]);
|
|
983
|
-
const handleRelease = useCallback(
|
|
984
|
-
(
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
// stays open — a dismissal slides out at the min-clamped height.
|
|
989
|
-
isSettlingAfterDragRef.current = open;
|
|
990
|
-
onDragEndRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragEndEvent);
|
|
991
|
-
},
|
|
992
|
-
[computeDetentInfo]
|
|
993
|
-
);
|
|
778
|
+
const handleRelease = useCallback(() => {
|
|
779
|
+
if (!isDraggingRef.current) return;
|
|
780
|
+
isDraggingRef.current = false;
|
|
781
|
+
onDragEndRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragEndEvent);
|
|
782
|
+
}, [computeDetentInfo]);
|
|
994
783
|
|
|
995
784
|
const { isNested, dismissAbove, descendants } = useSheetStack(
|
|
996
785
|
methodsRef,
|
|
@@ -1010,6 +799,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1010
799
|
// Mirror Android: translate this sheet down to match the deepest descendant's
|
|
1011
800
|
// top so the whole stack visually aligns. Cascades because every ancestor
|
|
1012
801
|
// re-runs whenever the stack (and thus its descendants) changes.
|
|
802
|
+
const wasCascadedRef = useRef(false);
|
|
1013
803
|
useEffect(() => {
|
|
1014
804
|
const parent = drawerContentRef.current;
|
|
1015
805
|
if (!parent) return;
|
|
@@ -1021,7 +811,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1021
811
|
if (!isOpen) return;
|
|
1022
812
|
const parentWrapper = parent.closest<HTMLElement>('[data-vaul-detached-wrapper]');
|
|
1023
813
|
|
|
1024
|
-
const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
814
|
+
const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')}), --snap-point-height ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
1025
815
|
const wrapperTransition = `clip-path ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
1026
816
|
const CLIP_NONE = 'inset(0px round 0px)';
|
|
1027
817
|
|
|
@@ -1045,11 +835,14 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1045
835
|
};
|
|
1046
836
|
|
|
1047
837
|
if (descendants.length === 0) {
|
|
838
|
+
if (!wasCascadedRef.current) return;
|
|
839
|
+
wasCascadedRef.current = false;
|
|
1048
840
|
parent.style.transition = transition;
|
|
1049
841
|
parent.style.transform = '';
|
|
1050
842
|
setClip(CLIP_NONE);
|
|
1051
843
|
return;
|
|
1052
844
|
}
|
|
845
|
+
wasCascadedRef.current = true;
|
|
1053
846
|
|
|
1054
847
|
// Track the immediate child's *actual* top: its cascade transform when it
|
|
1055
848
|
// was itself pushed behind a grandchild, else its own snap point. Mirrors
|
|
@@ -1209,11 +1002,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1209
1002
|
};
|
|
1210
1003
|
}, [isOpen, descendants.length]);
|
|
1211
1004
|
|
|
1212
|
-
// Definite-height flex layout (per-detent sizing) unless content must be
|
|
1213
|
-
// measured in natural flow: 'auto' detents (without a plugged scrollable —
|
|
1214
|
-
// those measure via `measureNaturalHeight`) and form-sheet content-fit sizing.
|
|
1215
|
-
const useSizedLayout = (!hasAutoDetent || hasBoundedScrollable) && !isFormSheet;
|
|
1216
|
-
|
|
1217
1005
|
const effectiveCornerRadius = cornerRadius ?? DEFAULT_CORNER_RADIUS;
|
|
1218
1006
|
|
|
1219
1007
|
// Shadow cast upward from the sheet's top edge toward the background. Matches
|
|
@@ -1399,14 +1187,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1399
1187
|
peekHeight={peekHeight}
|
|
1400
1188
|
initialAnimated={initialDetentAnimated}
|
|
1401
1189
|
detachedWrapperStyle={wrapperStyle}
|
|
1402
|
-
|
|
1403
|
-
contentHeight={
|
|
1404
|
-
// Bounded scrollable → the auto-size wrapper measures 0 (sized
|
|
1405
|
-
// layout is absolute), so feed vaul the natural height instead.
|
|
1406
|
-
hasAutoDetent && hasBoundedScrollable && scrollableAutoHeight > 0
|
|
1407
|
-
? scrollableAutoHeight
|
|
1408
|
-
: undefined
|
|
1409
|
-
}
|
|
1190
|
+
contentHeight={measuredContentHeight}
|
|
1410
1191
|
activeSnapPoint={activeSnapPoint}
|
|
1411
1192
|
setActiveSnapPoint={handleSetActiveSnapPoint}
|
|
1412
1193
|
{...snapPointsProps}
|
|
@@ -1431,64 +1212,29 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1431
1212
|
{accessibilityOptions?.paneTitle ?? 'Sheet'}
|
|
1432
1213
|
</Drawer.Title>
|
|
1433
1214
|
{grabber && <Drawer.Handle style={handleStyle} />}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
{isValidElement(header) ? header : createElement(header)}
|
|
1449
|
-
</View>
|
|
1450
|
-
)}
|
|
1451
|
-
{/* Content lays out naturally like native — fill only for auto
|
|
1452
|
-
detents with a plugged scrollable, where natural layout is
|
|
1453
|
-
circular (sheet height derives from the scroll content size).
|
|
1454
|
-
Unlike native, CSS won't cap the content to the container
|
|
1455
|
-
(no fit-content flex basis), so the fill is still needed here. */}
|
|
1215
|
+
<div
|
|
1216
|
+
ref={sizedLayoutRef}
|
|
1217
|
+
style={sizedLayoutStyle}
|
|
1218
|
+
data-vaul-scroll-locked={isScrollLocked ? '' : undefined}
|
|
1219
|
+
>
|
|
1220
|
+
{header && (
|
|
1221
|
+
<View ref={headerElRef} style={resolvedHeaderStyle} onLayout={handleHeaderLayout}>
|
|
1222
|
+
{isValidElement(header) ? header : createElement(header)}
|
|
1223
|
+
</View>
|
|
1224
|
+
)}
|
|
1225
|
+
<View ref={contentRef} style={[contentStyle, style]} onLayout={handleContentLayout}>
|
|
1226
|
+
{children}
|
|
1227
|
+
</View>
|
|
1228
|
+
{footer && !absoluteFooter && (
|
|
1456
1229
|
<View
|
|
1457
|
-
ref={
|
|
1458
|
-
style={
|
|
1459
|
-
onLayout={
|
|
1230
|
+
ref={footerElRef}
|
|
1231
|
+
style={[relativeFooterStyle, resolvedFooterStyle]}
|
|
1232
|
+
onLayout={handleFooterLayout}
|
|
1460
1233
|
>
|
|
1461
|
-
{
|
|
1462
|
-
</View>
|
|
1463
|
-
{footer && !absoluteFooter && (
|
|
1464
|
-
<View
|
|
1465
|
-
ref={footerElRef}
|
|
1466
|
-
style={[relativeFooterStyle, resolvedFooterStyle]}
|
|
1467
|
-
onLayout={handleFooterLayout}
|
|
1468
|
-
>
|
|
1469
|
-
{isValidElement(footer) ? footer : createElement(footer)}
|
|
1470
|
-
</View>
|
|
1471
|
-
)}
|
|
1472
|
-
</div>
|
|
1473
|
-
) : (
|
|
1474
|
-
// Natural flow so vaul can measure content height — required for
|
|
1475
|
-
// 'auto' detents and form-sheet content-fit sizing.
|
|
1476
|
-
<>
|
|
1477
|
-
{header && (
|
|
1478
|
-
<View ref={headerElRef} style={resolvedHeaderStyle} onLayout={handleHeaderLayout}>
|
|
1479
|
-
{isValidElement(header) ? header : createElement(header)}
|
|
1480
|
-
</View>
|
|
1481
|
-
)}
|
|
1482
|
-
<View ref={contentRef} style={style} onLayout={handleContentLayout}>
|
|
1483
|
-
{children}
|
|
1234
|
+
{isValidElement(footer) ? footer : createElement(footer)}
|
|
1484
1235
|
</View>
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
{isValidElement(footer) ? footer : createElement(footer)}
|
|
1488
|
-
</View>
|
|
1489
|
-
)}
|
|
1490
|
-
</>
|
|
1491
|
-
)}
|
|
1236
|
+
)}
|
|
1237
|
+
</div>
|
|
1492
1238
|
</Drawer.Content>
|
|
1493
1239
|
</Drawer.Portal>
|
|
1494
1240
|
</Drawer.Root>
|
|
@@ -1526,12 +1272,11 @@ const sizedLayoutStyle: React.CSSProperties = {
|
|
|
1526
1272
|
height: SIZED_LAYOUT_HEIGHT,
|
|
1527
1273
|
display: 'flex',
|
|
1528
1274
|
flexDirection: 'column',
|
|
1275
|
+
boxSizing: 'border-box',
|
|
1276
|
+
paddingBottom: 'inherit',
|
|
1529
1277
|
};
|
|
1530
1278
|
|
|
1531
|
-
const
|
|
1532
|
-
flex: 1,
|
|
1533
|
-
minHeight: 0,
|
|
1534
|
-
} as const;
|
|
1279
|
+
const contentStyle = { flexShrink: 1, minHeight: 0 } as const;
|
|
1535
1280
|
|
|
1536
1281
|
const visuallyHiddenStyle: React.CSSProperties = {
|
|
1537
1282
|
position: 'absolute',
|