@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
|
@@ -7,6 +7,7 @@ import { measurePeekContentHeight, TrueSheetPeekContext } from "./TrueSheetPeek.
|
|
|
7
7
|
import { usePortalContainer, useRegisterSheet, useSheetStack } from "./TrueSheetProvider.web.js";
|
|
8
8
|
import { COLOR_SURFACE_CONTAINER_LOW_DARK, COLOR_SURFACE_CONTAINER_LOW_LIGHT, DEFAULT_PLACEMENT_OFFSET, DEFAULT_CORNER_RADIUS, DEFAULT_DETACHED_OFFSET, DEFAULT_FORM_SHEET_HEIGHT_RATIO, DEFAULT_FORM_SHEET_WIDTH, DEFAULT_GRABBER_COLOR_DARK, DEFAULT_GRABBER_COLOR_LIGHT, DEFAULT_GRABBER_HEIGHT, DEFAULT_GRABBER_TOP_MARGIN, DEFAULT_GRABBER_WIDTH, DEFAULT_MAX_WIDTH } from "./web/constants.js";
|
|
9
9
|
import { getDOMElement } from "./web/dom.js";
|
|
10
|
+
import { observeSheetLayout } from "./web/layout.js";
|
|
10
11
|
import { Drawer } from "./web/vaul/index.js";
|
|
11
12
|
import { DEFAULT_PEEK_HEIGHT, DRAG_CLASS, TRANSITIONS } from "./web/vaul/constants.js";
|
|
12
13
|
|
|
@@ -14,7 +15,7 @@ import { DEFAULT_PEEK_HEIGHT, DRAG_CLASS, TRANSITIONS } from "./web/vaul/constan
|
|
|
14
15
|
// ScrollView refs expose the scroll node directly (with helpers attached);
|
|
15
16
|
// list refs (e.g. FlatList) expose it via getScrollableNode(). The node's
|
|
16
17
|
// first child is the content container.
|
|
17
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
18
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
19
|
const getScrollableElement = scrollable => {
|
|
19
20
|
if (!scrollable) return null;
|
|
20
21
|
if (scrollable instanceof HTMLElement) return scrollable;
|
|
@@ -104,7 +105,6 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
104
105
|
}, [validDetents]);
|
|
105
106
|
const validDetentsRef = useRef(validDetents);
|
|
106
107
|
validDetentsRef.current = validDetents;
|
|
107
|
-
const hasAutoDetent = validDetents.includes('auto');
|
|
108
108
|
const handleSetActiveSnapPoint = useCallback(snapPoint => {
|
|
109
109
|
setActiveSnapPoint(snapPoint == null ? null : typeof snapPoint === 'number' || snapPoint === 'auto' || snapPoint === 'peek' ? snapPoint : null);
|
|
110
110
|
}, []);
|
|
@@ -235,8 +235,6 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
235
235
|
// An absolute (floating) header overlaps the content, so it contributes no
|
|
236
236
|
// height to the 'auto' detent measurement.
|
|
237
237
|
const absoluteHeader = headerOptions?.position === 'absolute';
|
|
238
|
-
const absoluteHeaderRef = useRef(absoluteHeader);
|
|
239
|
-
absoluteHeaderRef.current = absoluteHeader;
|
|
240
238
|
const resolvedHeaderStyle = absoluteHeader ? [absoluteHeaderStyle, headerStyle] : headerStyle;
|
|
241
239
|
|
|
242
240
|
// Same for an absolute (floating) footer — rendered via vaul's
|
|
@@ -263,119 +261,54 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
263
261
|
// A relative footer is laid out below the content, so it's pushed off-screen
|
|
264
262
|
// at the peek detent and contributes no height.
|
|
265
263
|
const peekHeight = (header ? headerHeight : 0) + (footer && absoluteFooter ? footerHeight : 0) + peekContentHeight || DEFAULT_PEEK_HEIGHT;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
// `naturalHeight`.
|
|
272
|
-
const [hasBoundedScrollable, setHasBoundedScrollable] = useState(false);
|
|
273
|
-
const [scrollableAutoHeight, setScrollableAutoHeight] = useState(0);
|
|
274
|
-
const detectedScrollerRef = useRef(null);
|
|
275
|
-
// Bottom padding applied to the plugged scrollable for an absolute footer
|
|
276
|
-
// (see the footer inset effect below)
|
|
277
|
-
const footerInsetRef = useRef(0);
|
|
278
|
-
|
|
279
|
-
// Natural content height (header + content + footer, with a detected
|
|
280
|
-
// scrollable's viewport replaced by its content size) — the height the
|
|
281
|
-
// content wants regardless of the sheet's bounds. An absolute footer's inset
|
|
282
|
-
// on the scrollable counts like a relative footer's height, so the content
|
|
283
|
-
// ends above the footer (mirrors native's auto detent).
|
|
284
|
-
const measureNaturalHeight = useCallback(() => {
|
|
285
|
-
const contentEl = getDOMElement(contentRef.current);
|
|
286
|
-
if (!contentEl || !contentEl.isConnected) return 0;
|
|
287
|
-
const headerEl = absoluteHeaderRef.current ? null : getDOMElement(headerElRef.current);
|
|
288
|
-
const footerEl = absoluteFooterRef.current ? null : getDOMElement(footerElRef.current);
|
|
289
|
-
let height = (headerEl?.offsetHeight ?? 0) + (footerEl?.offsetHeight ?? 0) + contentEl.offsetHeight;
|
|
290
|
-
const scroller = detectedScrollerRef.current;
|
|
291
|
-
const scrollContent = scroller?.firstElementChild;
|
|
292
|
-
if (scroller?.isConnected && scrollContent instanceof HTMLElement) {
|
|
293
|
-
height += scrollContent.offsetHeight - scroller.clientHeight + footerInsetRef.current;
|
|
294
|
-
}
|
|
295
|
-
return Math.max(0, height);
|
|
296
|
-
}, []);
|
|
264
|
+
const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
|
|
265
|
+
const naturalHeightRef = useRef(0);
|
|
266
|
+
const insetBehavior = scrollableOptions?.contentInsetAdjustment ?? 'automatic';
|
|
267
|
+
const footerInsetAdjustment = Boolean(footer) && absoluteFooter && (insetBehavior === 'automatic' || insetBehavior === 'footer');
|
|
268
|
+
const measureContent = validDetents.includes('auto') || isFormSheet;
|
|
297
269
|
useEffect(() => {
|
|
298
|
-
if (!isOpen
|
|
270
|
+
if (!isOpen) return undefined;
|
|
299
271
|
let canceled = false;
|
|
300
272
|
let rafId = 0;
|
|
301
|
-
let
|
|
302
|
-
let resizeObserver = null;
|
|
273
|
+
let observer;
|
|
303
274
|
const attach = () => {
|
|
304
275
|
if (canceled) return;
|
|
305
|
-
const
|
|
306
|
-
|
|
276
|
+
const layoutEl = sizedLayoutRef.current;
|
|
277
|
+
const drawer = drawerContentRef.current;
|
|
278
|
+
if (!drawer || !layoutEl?.isConnected) {
|
|
307
279
|
// Radix Presence defers the portal mount; poll until the drawer is live.
|
|
308
280
|
rafId = window.requestAnimationFrame(attach);
|
|
309
281
|
return;
|
|
310
282
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
// changes are already covered by the ResizeObserver.
|
|
326
|
-
if (observed && observed.contentEl === contentEl && contentEl?.isConnected && observed.headerEl === headerEl && observed.scroller.isConnected && observed.scrollContent === observed.scroller.firstElementChild) {
|
|
327
|
-
return;
|
|
283
|
+
observer = observeSheetLayout({
|
|
284
|
+
drawer,
|
|
285
|
+
layout: layoutEl,
|
|
286
|
+
getContent: () => getDOMElement(contentRef.current),
|
|
287
|
+
getHeader: () => getDOMElement(headerElRef.current),
|
|
288
|
+
getFooter: () => getDOMElement(footerElRef.current),
|
|
289
|
+
getScrollable: () => getScrollableElement(scrollableRef?.current),
|
|
290
|
+
absoluteHeader,
|
|
291
|
+
absoluteFooter,
|
|
292
|
+
footerInsetAdjustment,
|
|
293
|
+
measureContent,
|
|
294
|
+
onHeightChange: height => {
|
|
295
|
+
naturalHeightRef.current = height;
|
|
296
|
+
setMeasuredContentHeight(height);
|
|
328
297
|
}
|
|
329
|
-
const resolved = contentEl && contentEl.isConnected ? getScrollableElement(scrollableRef?.current) : null;
|
|
330
|
-
const scroller = resolved?.isConnected ? resolved : null;
|
|
331
|
-
detectedScrollerRef.current = scroller;
|
|
332
|
-
setHasBoundedScrollable(scroller != null);
|
|
333
|
-
observed = scroller ? {
|
|
334
|
-
contentEl,
|
|
335
|
-
headerEl,
|
|
336
|
-
scroller,
|
|
337
|
-
scrollContent: scroller.firstElementChild
|
|
338
|
-
} : null;
|
|
339
|
-
resizeObserver?.disconnect();
|
|
340
|
-
resizeObserver = new ResizeObserver(measure);
|
|
341
|
-
if (contentEl?.isConnected) resizeObserver.observe(contentEl);
|
|
342
|
-
if (headerEl) resizeObserver.observe(headerEl);
|
|
343
|
-
if (scroller) {
|
|
344
|
-
resizeObserver.observe(scroller);
|
|
345
|
-
if (scroller.firstElementChild) resizeObserver.observe(scroller.firstElementChild);
|
|
346
|
-
}
|
|
347
|
-
measure();
|
|
348
|
-
};
|
|
349
|
-
observe();
|
|
350
|
-
// Watch the whole drawer subtree — catches the scrollable mounting/
|
|
351
|
-
// unmounting AND the layout-branch swap, which remounts the content node
|
|
352
|
-
// (a content-scoped observer would go stale after the swap).
|
|
353
|
-
mutationObserver = new MutationObserver(observe);
|
|
354
|
-
mutationObserver.observe(drawerEl, {
|
|
355
|
-
childList: true,
|
|
356
|
-
subtree: true
|
|
357
298
|
});
|
|
358
299
|
};
|
|
359
300
|
rafId = window.requestAnimationFrame(attach);
|
|
360
301
|
return () => {
|
|
361
302
|
canceled = true;
|
|
362
303
|
window.cancelAnimationFrame(rafId);
|
|
363
|
-
|
|
364
|
-
resizeObserver?.disconnect();
|
|
304
|
+
observer?.disconnect();
|
|
365
305
|
};
|
|
366
|
-
}, [isOpen,
|
|
306
|
+
}, [isOpen, absoluteHeader, absoluteFooter, footerInsetAdjustment, measureContent, scrollableRef]);
|
|
367
307
|
|
|
368
308
|
// Below the last detent a vertical touch pan moves the sheet, not the
|
|
369
309
|
// content — `[data-vaul-scroll-locked]` disables vertical touch panning on
|
|
370
310
|
// the scroll container and everything inside it (see vaul/style.css).
|
|
371
311
|
const isScrollLocked = validDetents.length > 0 && activeSnapPoint !== validDetents[validDetents.length - 1];
|
|
372
|
-
|
|
373
|
-
// Vaul measures the auto-size wrapper's offsetHeight (always, post fork).
|
|
374
|
-
// Track it here so the form sheet can size its card to fit content,
|
|
375
|
-
// clamped between a minimum ratio of the viewport and a maximum derived
|
|
376
|
-
// from `detachedOffset` (the breathing room left at top + bottom of the
|
|
377
|
-
// floating card).
|
|
378
|
-
const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
|
|
379
312
|
const effectiveMaxContentHeight = useMemo(() => {
|
|
380
313
|
if (maxContentHeight !== undefined) return maxContentHeight;
|
|
381
314
|
if (!isFormSheet) return undefined;
|
|
@@ -438,16 +371,14 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
438
371
|
effectiveDetachedOffset,
|
|
439
372
|
effectiveMaxContentHeight,
|
|
440
373
|
peekHeight,
|
|
441
|
-
peekContentHeight
|
|
442
|
-
measuredContentHeight
|
|
374
|
+
peekContentHeight
|
|
443
375
|
});
|
|
444
376
|
geometryInputsRef.current = {
|
|
445
377
|
effectiveDetached,
|
|
446
378
|
effectiveDetachedOffset,
|
|
447
379
|
effectiveMaxContentHeight,
|
|
448
380
|
peekHeight,
|
|
449
|
-
peekContentHeight
|
|
450
|
-
measuredContentHeight
|
|
381
|
+
peekContentHeight
|
|
451
382
|
};
|
|
452
383
|
const computeDetentGeometry = useCallback(() => {
|
|
453
384
|
const inputs = geometryInputsRef.current;
|
|
@@ -455,12 +386,9 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
455
386
|
const effectiveH = inputs.effectiveDetached ? windowH - inputs.effectiveDetachedOffset : windowH;
|
|
456
387
|
// Matches vaul's height ceiling: min(effectiveH, maxContentHeight).
|
|
457
388
|
const ceiling = inputs.effectiveMaxContentHeight !== undefined ? Math.min(effectiveH, inputs.effectiveMaxContentHeight) : effectiveH;
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
|
|
461
|
-
// so it reads 0 until the ResizeObserver fires). Falls back to the state
|
|
462
|
-
// value, then to vaul's pre-measure fallback (effectiveHeight / 2).
|
|
463
|
-
const measuredHeight = measureNaturalHeight() || inputs.measuredContentHeight;
|
|
389
|
+
// Reuse the intrinsic measurement during position events so animation
|
|
390
|
+
// frames don't clone the content tree again.
|
|
391
|
+
const measuredHeight = naturalHeightRef.current;
|
|
464
392
|
const autoHeight = Math.min(measuredHeight > 0 ? measuredHeight : effectiveH / 2, ceiling);
|
|
465
393
|
|
|
466
394
|
// 'peek' is measured live from the DOM (offsetHeight is layout-based, so
|
|
@@ -491,7 +419,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
491
419
|
positions,
|
|
492
420
|
values
|
|
493
421
|
};
|
|
494
|
-
}, [
|
|
422
|
+
}, []);
|
|
495
423
|
|
|
496
424
|
// Detent info for lifecycle events. Position/detent come from the active
|
|
497
425
|
// detent's target geometry — not the live DOM rect — so willPresent (drawer
|
|
@@ -578,46 +506,9 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
578
506
|
detent: values[count - 1]
|
|
579
507
|
};
|
|
580
508
|
}, [computeDetentGeometry]);
|
|
581
|
-
|
|
582
|
-
// Mirror native synchronous layout: while dragging (and through the post-
|
|
583
|
-
// release settle) the sized layout tracks the sheet's visible height in
|
|
584
|
-
// realtime, clamped to the smallest detent — dragging below it slides the
|
|
585
|
-
// sheet out without resizing. Once settled, the inline height is cleared so
|
|
586
|
-
// the CSS calc() owns sizing again (adapts to viewport resizes at rest).
|
|
587
509
|
const sizedLayoutRef = useRef(null);
|
|
588
510
|
const isDraggingRef = useRef(false);
|
|
589
|
-
const isSettlingAfterDragRef = useRef(false);
|
|
590
|
-
const updateSizedLayoutHeight = useCallback(position => {
|
|
591
|
-
const node = sizedLayoutRef.current;
|
|
592
|
-
if (!node) return;
|
|
593
|
-
const {
|
|
594
|
-
effectiveH,
|
|
595
|
-
ceiling,
|
|
596
|
-
positions
|
|
597
|
-
} = computeDetentGeometry();
|
|
598
|
-
if (positions.length === 0) return;
|
|
599
|
-
if (isSettlingAfterDragRef.current) {
|
|
600
|
-
const snap = activeSnapPointRef.current;
|
|
601
|
-
const index = snap != null ? validDetentsRef.current.indexOf(snap) : -1;
|
|
602
|
-
const target = index >= 0 ? positions[index] : null;
|
|
603
|
-
if (target != null && Math.abs(position - target) < 1) {
|
|
604
|
-
isSettlingAfterDragRef.current = false;
|
|
605
|
-
// Restore the calc — height is an inline style, so clearing it would
|
|
606
|
-
// leave the div unsized (React won't re-apply it without a render)
|
|
607
|
-
node.style.height = SIZED_LAYOUT_HEIGHT;
|
|
608
|
-
return;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// positions[0] is the smallest detent's top-Y — its height is the clamp floor.
|
|
613
|
-
const minHeight = effectiveH - positions[0];
|
|
614
|
-
const height = Math.min(Math.max(effectiveH - position, minHeight), ceiling);
|
|
615
|
-
node.style.height = `${height}px`;
|
|
616
|
-
}, [computeDetentGeometry]);
|
|
617
511
|
const handlePositionChange = useCallback(position => {
|
|
618
|
-
if (isDraggingRef.current || isSettlingAfterDragRef.current) {
|
|
619
|
-
updateSizedLayoutHeight(position);
|
|
620
|
-
}
|
|
621
512
|
const {
|
|
622
513
|
index,
|
|
623
514
|
detent
|
|
@@ -630,91 +521,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
630
521
|
realtime: true
|
|
631
522
|
}
|
|
632
523
|
});
|
|
633
|
-
}, [interpolateFromPosition
|
|
634
|
-
|
|
635
|
-
// Web mirror of native's footer inset (`contentInsetAdjustment`
|
|
636
|
-
// 'automatic' | 'footer'): pad the plugged scrollable by how much the
|
|
637
|
-
// absolute footer covers it. Measured from layout offsets (not client rects)
|
|
638
|
-
// so in-flight transforms don't skew it — the footer's bottom sits at the
|
|
639
|
-
// visible bottom of the layout container, so only a scroller reaching that
|
|
640
|
-
// edge gets an inset, and a safe-area lift (or a short scroller) shrinks it.
|
|
641
|
-
// Padding on the scroll container extends its scrollable overflow, like a
|
|
642
|
-
// native content inset.
|
|
643
|
-
const insetBehavior = scrollableOptions?.contentInsetAdjustment ?? 'automatic';
|
|
644
|
-
const footerInsetAdjustment = Boolean(footer) && absoluteFooter && (insetBehavior === 'automatic' || insetBehavior === 'footer');
|
|
645
|
-
useEffect(() => {
|
|
646
|
-
if (!isOpen || !footerInsetAdjustment || !scrollableRef) return undefined;
|
|
647
|
-
let canceled = false;
|
|
648
|
-
let rafId = 0;
|
|
649
|
-
let mutationObserver = null;
|
|
650
|
-
let resizeObserver = null;
|
|
651
|
-
let scroller = null;
|
|
652
|
-
const reset = () => {
|
|
653
|
-
if (scroller) scroller.style.paddingBottom = '';
|
|
654
|
-
footerInsetRef.current = 0;
|
|
655
|
-
};
|
|
656
|
-
const apply = () => {
|
|
657
|
-
const footerEl = getDOMElement(footerElRef.current);
|
|
658
|
-
const container = sizedLayoutRef.current ?? drawerContentRef.current;
|
|
659
|
-
if (!scroller?.isConnected || !footerEl || !container) return;
|
|
660
|
-
|
|
661
|
-
// Measure without our own padding so an unbounded scroller (sized by its
|
|
662
|
-
// content) doesn't grow by the inset and feed back into the overlap.
|
|
663
|
-
scroller.style.paddingBottom = '';
|
|
664
|
-
let bottom = scroller.offsetTop + scroller.offsetHeight;
|
|
665
|
-
let el = scroller.offsetParent;
|
|
666
|
-
while (el && el !== container) {
|
|
667
|
-
bottom += el.offsetTop;
|
|
668
|
-
el = el.offsetParent;
|
|
669
|
-
}
|
|
670
|
-
const footerTop = container.offsetHeight - footerEl.offsetHeight;
|
|
671
|
-
const inset = el ? Math.max(0, bottom - footerTop) : 0;
|
|
672
|
-
footerInsetRef.current = inset;
|
|
673
|
-
scroller.style.paddingBottom = inset > 0 ? `${inset}px` : '';
|
|
674
|
-
};
|
|
675
|
-
|
|
676
|
-
// (Re)resolve the plugged scrollable — a conditional remount swaps the node
|
|
677
|
-
const observe = () => {
|
|
678
|
-
const resolved = getScrollableElement(scrollableRef.current);
|
|
679
|
-
const next = resolved?.isConnected ? resolved : null;
|
|
680
|
-
if (next === scroller && resizeObserver) return;
|
|
681
|
-
reset();
|
|
682
|
-
resizeObserver?.disconnect();
|
|
683
|
-
resizeObserver = null;
|
|
684
|
-
scroller = next;
|
|
685
|
-
if (!scroller) return;
|
|
686
|
-
resizeObserver = new ResizeObserver(apply);
|
|
687
|
-
resizeObserver.observe(scroller);
|
|
688
|
-
const footerEl = getDOMElement(footerElRef.current);
|
|
689
|
-
if (footerEl) resizeObserver.observe(footerEl);
|
|
690
|
-
const container = sizedLayoutRef.current ?? drawerContentRef.current;
|
|
691
|
-
if (container) resizeObserver.observe(container);
|
|
692
|
-
apply();
|
|
693
|
-
};
|
|
694
|
-
const attach = () => {
|
|
695
|
-
if (canceled) return;
|
|
696
|
-
const drawerEl = drawerContentRef.current;
|
|
697
|
-
if (!drawerEl || !drawerEl.isConnected) {
|
|
698
|
-
// Radix Presence defers the portal mount; poll until the drawer is live.
|
|
699
|
-
rafId = window.requestAnimationFrame(attach);
|
|
700
|
-
return;
|
|
701
|
-
}
|
|
702
|
-
observe();
|
|
703
|
-
mutationObserver = new MutationObserver(observe);
|
|
704
|
-
mutationObserver.observe(drawerEl, {
|
|
705
|
-
childList: true,
|
|
706
|
-
subtree: true
|
|
707
|
-
});
|
|
708
|
-
};
|
|
709
|
-
rafId = window.requestAnimationFrame(attach);
|
|
710
|
-
return () => {
|
|
711
|
-
canceled = true;
|
|
712
|
-
window.cancelAnimationFrame(rafId);
|
|
713
|
-
mutationObserver?.disconnect();
|
|
714
|
-
resizeObserver?.disconnect();
|
|
715
|
-
reset();
|
|
716
|
-
};
|
|
717
|
-
}, [isOpen, footerInsetAdjustment, scrollableRef]);
|
|
524
|
+
}, [interpolateFromPosition]);
|
|
718
525
|
|
|
719
526
|
// Fire onMount once after first render. React-mount is the earliest point
|
|
720
527
|
// the component is ready for imperative calls, matching the native
|
|
@@ -860,12 +667,9 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
860
667
|
nativeEvent: computeDetentInfo(true)
|
|
861
668
|
});
|
|
862
669
|
}, [computeDetentInfo]);
|
|
863
|
-
const handleRelease = useCallback((
|
|
670
|
+
const handleRelease = useCallback(() => {
|
|
864
671
|
if (!isDraggingRef.current) return;
|
|
865
672
|
isDraggingRef.current = false;
|
|
866
|
-
// Track the sized layout through the settle animation only when the sheet
|
|
867
|
-
// stays open — a dismissal slides out at the min-clamped height.
|
|
868
|
-
isSettlingAfterDragRef.current = open;
|
|
869
673
|
onDragEndRef.current?.({
|
|
870
674
|
nativeEvent: computeDetentInfo(true)
|
|
871
675
|
});
|
|
@@ -887,6 +691,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
887
691
|
// Mirror Android: translate this sheet down to match the deepest descendant's
|
|
888
692
|
// top so the whole stack visually aligns. Cascades because every ancestor
|
|
889
693
|
// re-runs whenever the stack (and thus its descendants) changes.
|
|
694
|
+
const wasCascadedRef = useRef(false);
|
|
890
695
|
useEffect(() => {
|
|
891
696
|
const parent = drawerContentRef.current;
|
|
892
697
|
if (!parent) return;
|
|
@@ -897,7 +702,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
897
702
|
// out — nothing to align with anymore.
|
|
898
703
|
if (!isOpen) return;
|
|
899
704
|
const parentWrapper = parent.closest('[data-vaul-detached-wrapper]');
|
|
900
|
-
const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
705
|
+
const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')}), --snap-point-height ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
901
706
|
const wrapperTransition = `clip-path ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
902
707
|
const CLIP_NONE = 'inset(0px round 0px)';
|
|
903
708
|
|
|
@@ -920,11 +725,14 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
920
725
|
parentWrapper.style.clipPath = next;
|
|
921
726
|
};
|
|
922
727
|
if (descendants.length === 0) {
|
|
728
|
+
if (!wasCascadedRef.current) return;
|
|
729
|
+
wasCascadedRef.current = false;
|
|
923
730
|
parent.style.transition = transition;
|
|
924
731
|
parent.style.transform = '';
|
|
925
732
|
setClip(CLIP_NONE);
|
|
926
733
|
return;
|
|
927
734
|
}
|
|
735
|
+
wasCascadedRef.current = true;
|
|
928
736
|
|
|
929
737
|
// Track the immediate child's *actual* top: its cascade transform when it
|
|
930
738
|
// was itself pushed behind a grandchild, else its own snap point. Mirrors
|
|
@@ -1088,11 +896,6 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
1088
896
|
window.cancelAnimationFrame(rafId);
|
|
1089
897
|
};
|
|
1090
898
|
}, [isOpen, descendants.length]);
|
|
1091
|
-
|
|
1092
|
-
// Definite-height flex layout (per-detent sizing) unless content must be
|
|
1093
|
-
// measured in natural flow: 'auto' detents (without a plugged scrollable —
|
|
1094
|
-
// those measure via `measureNaturalHeight`) and form-sheet content-fit sizing.
|
|
1095
|
-
const useSizedLayout = (!hasAutoDetent || hasBoundedScrollable) && !isFormSheet;
|
|
1096
899
|
const effectiveCornerRadius = cornerRadius ?? DEFAULT_CORNER_RADIUS;
|
|
1097
900
|
|
|
1098
901
|
// Shadow cast upward from the sheet's top edge toward the background. Matches
|
|
@@ -1231,11 +1034,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
1231
1034
|
peekHeight: peekHeight,
|
|
1232
1035
|
initialAnimated: initialDetentAnimated,
|
|
1233
1036
|
detachedWrapperStyle: wrapperStyle,
|
|
1234
|
-
|
|
1235
|
-
contentHeight:
|
|
1236
|
-
// Bounded scrollable → the auto-size wrapper measures 0 (sized
|
|
1237
|
-
// layout is absolute), so feed vaul the natural height instead.
|
|
1238
|
-
hasAutoDetent && hasBoundedScrollable && scrollableAutoHeight > 0 ? scrollableAutoHeight : undefined,
|
|
1037
|
+
contentHeight: measuredContentHeight,
|
|
1239
1038
|
activeSnapPoint: activeSnapPoint,
|
|
1240
1039
|
setActiveSnapPoint: handleSetActiveSnapPoint,
|
|
1241
1040
|
...snapPointsProps,
|
|
@@ -1261,15 +1060,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
1261
1060
|
children: accessibilityOptions?.paneTitle ?? 'Sheet'
|
|
1262
1061
|
}), grabber && /*#__PURE__*/_jsx(Drawer.Handle, {
|
|
1263
1062
|
style: handleStyle
|
|
1264
|
-
}),
|
|
1265
|
-
/*#__PURE__*/
|
|
1266
|
-
// vaul wraps children in `[data-vaul-auto-size-wrapper]` (display:
|
|
1267
|
-
// flow-root) which doesn't honor descendant flex layout. Use an
|
|
1268
|
-
// absolute fill sized to the visible portion (via vaul's
|
|
1269
|
-
// `--snap-point-height` var) so the inner flex column has a
|
|
1270
|
-
// definite height for flex layouts to fill — mirrors native, where
|
|
1271
|
-
// the container is sized to the sheet's visible height per detent.
|
|
1272
|
-
_jsxs("div", {
|
|
1063
|
+
}), /*#__PURE__*/_jsxs("div", {
|
|
1273
1064
|
ref: sizedLayoutRef,
|
|
1274
1065
|
style: sizedLayoutStyle,
|
|
1275
1066
|
"data-vaul-scroll-locked": isScrollLocked ? '' : undefined,
|
|
@@ -1280,7 +1071,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
1280
1071
|
children: /*#__PURE__*/isValidElement(header) ? header : /*#__PURE__*/createElement(header)
|
|
1281
1072
|
}), /*#__PURE__*/_jsx(View, {
|
|
1282
1073
|
ref: contentRef,
|
|
1283
|
-
style:
|
|
1074
|
+
style: [contentStyle, style],
|
|
1284
1075
|
onLayout: handleContentLayout,
|
|
1285
1076
|
children: children
|
|
1286
1077
|
}), footer && !absoluteFooter && /*#__PURE__*/_jsx(View, {
|
|
@@ -1289,27 +1080,6 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
1289
1080
|
onLayout: handleFooterLayout,
|
|
1290
1081
|
children: /*#__PURE__*/isValidElement(footer) ? footer : /*#__PURE__*/createElement(footer)
|
|
1291
1082
|
})]
|
|
1292
|
-
}) :
|
|
1293
|
-
/*#__PURE__*/
|
|
1294
|
-
// Natural flow so vaul can measure content height — required for
|
|
1295
|
-
// 'auto' detents and form-sheet content-fit sizing.
|
|
1296
|
-
_jsxs(_Fragment, {
|
|
1297
|
-
children: [header && /*#__PURE__*/_jsx(View, {
|
|
1298
|
-
ref: headerElRef,
|
|
1299
|
-
style: resolvedHeaderStyle,
|
|
1300
|
-
onLayout: handleHeaderLayout,
|
|
1301
|
-
children: /*#__PURE__*/isValidElement(header) ? header : /*#__PURE__*/createElement(header)
|
|
1302
|
-
}), /*#__PURE__*/_jsx(View, {
|
|
1303
|
-
ref: contentRef,
|
|
1304
|
-
style: style,
|
|
1305
|
-
onLayout: handleContentLayout,
|
|
1306
|
-
children: children
|
|
1307
|
-
}), footer && !absoluteFooter && /*#__PURE__*/_jsx(View, {
|
|
1308
|
-
ref: footerElRef,
|
|
1309
|
-
style: resolvedFooterStyle,
|
|
1310
|
-
onLayout: handleFooterLayout,
|
|
1311
|
-
children: /*#__PURE__*/isValidElement(footer) ? footer : /*#__PURE__*/createElement(footer)
|
|
1312
|
-
})]
|
|
1313
1083
|
})]
|
|
1314
1084
|
})]
|
|
1315
1085
|
})
|
|
@@ -1343,10 +1113,12 @@ const sizedLayoutStyle = {
|
|
|
1343
1113
|
right: 0,
|
|
1344
1114
|
height: SIZED_LAYOUT_HEIGHT,
|
|
1345
1115
|
display: 'flex',
|
|
1346
|
-
flexDirection: 'column'
|
|
1116
|
+
flexDirection: 'column',
|
|
1117
|
+
boxSizing: 'border-box',
|
|
1118
|
+
paddingBottom: 'inherit'
|
|
1347
1119
|
};
|
|
1348
|
-
const
|
|
1349
|
-
|
|
1120
|
+
const contentStyle = {
|
|
1121
|
+
flexShrink: 1,
|
|
1350
1122
|
minHeight: 0
|
|
1351
1123
|
};
|
|
1352
1124
|
const visuallyHiddenStyle = {
|