@lodev09/react-native-true-sheet 4.0.0-beta.14 → 4.0.0-beta.16
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 +28 -6
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +99 -7
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +12 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +11 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +18 -7
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +40 -39
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +18 -8
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +14 -8
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +8 -1
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +32 -5
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +3 -0
- package/ios/TrueSheetContainerView.h +17 -4
- package/ios/TrueSheetContainerView.mm +23 -6
- package/ios/TrueSheetContentView.h +13 -1
- package/ios/TrueSheetContentView.mm +93 -35
- package/ios/TrueSheetFooterView.mm +46 -16
- package/ios/TrueSheetView.mm +30 -28
- package/ios/TrueSheetViewController.h +21 -1
- package/ios/TrueSheetViewController.mm +54 -16
- package/ios/core/TrueSheetDetentCalculator.h +6 -1
- package/ios/core/TrueSheetDetentCalculator.mm +9 -4
- package/ios/core/TrueSheetKeyboardObserver.mm +2 -0
- package/lib/module/TrueSheet.js +8 -5
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +93 -218
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/TrueSheetPeek.web.js +20 -15
- package/lib/module/TrueSheetPeek.web.js.map +1 -1
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +5 -3
- package/lib/module/navigation/expo-router/index.js +5 -4
- package/lib/module/navigation/expo-router/index.js.map +1 -1
- package/lib/module/web/constants.js +1 -1
- package/lib/module/web/constants.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.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +64 -25
- 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/TrueSheetPeek.web.d.ts +8 -4
- package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +5 -3
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/navigation/expo-router/index.d.ts +5 -3
- package/lib/typescript/src/navigation/expo-router/index.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/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 +7 -6
- package/src/TrueSheet.tsx +7 -4
- package/src/TrueSheet.types.ts +67 -25
- package/src/TrueSheet.web.tsx +132 -263
- package/src/TrueSheetPeek.web.tsx +25 -23
- package/src/fabric/TrueSheetViewNativeComponent.ts +5 -3
- package/src/navigation/expo-router/index.ts +8 -11
- package/src/navigation/types.ts +3 -2
- package/src/web/constants.ts +1 -1
- 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
|
@@ -35,12 +35,16 @@ import type {
|
|
|
35
35
|
WillFocusEvent,
|
|
36
36
|
WillPresentEvent,
|
|
37
37
|
} from './TrueSheet.types';
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
measurePeekContentHeight,
|
|
40
|
+
TrueSheetPeekContext,
|
|
41
|
+
type TrueSheetPeekContextValue,
|
|
42
|
+
} from './TrueSheetPeek.web';
|
|
39
43
|
import { usePortalContainer, useRegisterSheet, useSheetStack } from './TrueSheetProvider.web';
|
|
40
44
|
import {
|
|
41
45
|
COLOR_SURFACE_CONTAINER_LOW_DARK,
|
|
42
46
|
COLOR_SURFACE_CONTAINER_LOW_LIGHT,
|
|
43
|
-
|
|
47
|
+
DEFAULT_PLACEMENT_OFFSET,
|
|
44
48
|
DEFAULT_CORNER_RADIUS,
|
|
45
49
|
DEFAULT_DETACHED_OFFSET,
|
|
46
50
|
DEFAULT_FORM_SHEET_HEIGHT_RATIO,
|
|
@@ -53,6 +57,7 @@ import {
|
|
|
53
57
|
DEFAULT_MAX_WIDTH,
|
|
54
58
|
} from './web/constants';
|
|
55
59
|
import { getDOMElement } from './web/dom';
|
|
60
|
+
import { observeSheetLayout } from './web/layout';
|
|
56
61
|
import { Drawer } from './web/vaul';
|
|
57
62
|
import { DEFAULT_PEEK_HEIGHT, DRAG_CLASS, TRANSITIONS } from './web/vaul/constants';
|
|
58
63
|
|
|
@@ -78,8 +83,8 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
78
83
|
backgroundColor: backgroundColorProp,
|
|
79
84
|
maxContentHeight,
|
|
80
85
|
maxContentWidth,
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
placement = 'automatic',
|
|
87
|
+
placementOffset = DEFAULT_PLACEMENT_OFFSET,
|
|
83
88
|
scrollableRef,
|
|
84
89
|
scrollableOptions,
|
|
85
90
|
grabber = true,
|
|
@@ -168,8 +173,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
168
173
|
const validDetentsRef = useRef(validDetents);
|
|
169
174
|
validDetentsRef.current = validDetents;
|
|
170
175
|
|
|
171
|
-
const hasAutoDetent = validDetents.includes('auto');
|
|
172
|
-
|
|
173
176
|
const handleSetActiveSnapPoint = useCallback((snapPoint: number | string | null) => {
|
|
174
177
|
setActiveSnapPoint(
|
|
175
178
|
snapPoint == null
|
|
@@ -291,13 +294,9 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
291
294
|
// within the content — measured by the peek against `contentRef`.
|
|
292
295
|
const [peekContentHeight, setPeekContentHeight] = useState(0);
|
|
293
296
|
const contentRef = useRef<View>(null);
|
|
294
|
-
const peekElRef = useRef<View>(null);
|
|
295
|
-
const peekContext = useMemo(() => ({ contentRef, peekRef: peekElRef, setPeekContentHeight }), []);
|
|
297
|
+
const peekElRef = useRef<View | null>(null);
|
|
296
298
|
|
|
297
|
-
|
|
298
|
-
// it moves it without resizing it, so re-measure its offset whenever the
|
|
299
|
-
// content view's layout changes.
|
|
300
|
-
const handleContentLayout = useCallback(() => {
|
|
299
|
+
const measurePeek = useCallback(() => {
|
|
301
300
|
const peekEl = getDOMElement(peekElRef.current);
|
|
302
301
|
const contentEl = getDOMElement(contentRef.current);
|
|
303
302
|
if (peekEl && contentEl) {
|
|
@@ -305,12 +304,35 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
305
304
|
}
|
|
306
305
|
}, []);
|
|
307
306
|
|
|
307
|
+
// Last-attached wins — during a screen swap the incoming peek mounts before
|
|
308
|
+
// the outgoing one unmounts, so the outgoing detach must not clear it.
|
|
309
|
+
const peekContext = useMemo<TrueSheetPeekContextValue>(
|
|
310
|
+
() => ({
|
|
311
|
+
contentRef,
|
|
312
|
+
attachPeek: (view) => {
|
|
313
|
+
peekElRef.current = view;
|
|
314
|
+
measurePeek();
|
|
315
|
+
},
|
|
316
|
+
detachPeek: (view) => {
|
|
317
|
+
if (peekElRef.current !== view) return;
|
|
318
|
+
peekElRef.current = null;
|
|
319
|
+
setPeekContentHeight(0);
|
|
320
|
+
},
|
|
321
|
+
measurePeek: (view) => {
|
|
322
|
+
if (peekElRef.current === view) measurePeek();
|
|
323
|
+
},
|
|
324
|
+
}),
|
|
325
|
+
[measurePeek]
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// The peek's own onLayout only fires when *it* resizes. Content added above
|
|
329
|
+
// it moves it without resizing it, so re-measure its offset whenever the
|
|
330
|
+
// content view's layout changes.
|
|
331
|
+
const handleContentLayout = measurePeek;
|
|
332
|
+
|
|
308
333
|
// An absolute (floating) header overlaps the content, so it contributes no
|
|
309
334
|
// height to the 'auto' detent measurement.
|
|
310
335
|
const absoluteHeader = headerOptions?.position === 'absolute';
|
|
311
|
-
const absoluteHeaderRef = useRef(absoluteHeader);
|
|
312
|
-
absoluteHeaderRef.current = absoluteHeader;
|
|
313
|
-
|
|
314
336
|
const resolvedHeaderStyle = absoluteHeader ? [absoluteHeaderStyle, headerStyle] : headerStyle;
|
|
315
337
|
|
|
316
338
|
// Same for an absolute (floating) footer — rendered via vaul's
|
|
@@ -346,108 +368,48 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
346
368
|
(footer && absoluteFooter ? footerHeight : 0) +
|
|
347
369
|
peekContentHeight || DEFAULT_PEEK_HEIGHT;
|
|
348
370
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
// Natural content height (header + content + footer, with a detected
|
|
359
|
-
// scrollable's viewport replaced by its content size) — the height the
|
|
360
|
-
// content wants regardless of the sheet's bounds.
|
|
361
|
-
const measureNaturalHeight = useCallback(() => {
|
|
362
|
-
const contentEl = getDOMElement(contentRef.current);
|
|
363
|
-
if (!contentEl || !contentEl.isConnected) return 0;
|
|
364
|
-
const headerEl = absoluteHeaderRef.current ? null : getDOMElement(headerElRef.current);
|
|
365
|
-
const footerEl = absoluteFooterRef.current ? null : getDOMElement(footerElRef.current);
|
|
366
|
-
let height =
|
|
367
|
-
(headerEl?.offsetHeight ?? 0) + (footerEl?.offsetHeight ?? 0) + contentEl.offsetHeight;
|
|
368
|
-
const scroller = detectedScrollerRef.current;
|
|
369
|
-
const scrollContent = scroller?.firstElementChild;
|
|
370
|
-
if (scroller?.isConnected && scrollContent instanceof HTMLElement) {
|
|
371
|
-
height += scrollContent.offsetHeight - scroller.clientHeight;
|
|
372
|
-
}
|
|
373
|
-
return Math.max(0, height);
|
|
374
|
-
}, []);
|
|
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;
|
|
375
379
|
|
|
376
380
|
useEffect(() => {
|
|
377
|
-
if (!isOpen
|
|
381
|
+
if (!isOpen) return undefined;
|
|
378
382
|
|
|
379
383
|
let canceled = false;
|
|
380
384
|
let rafId = 0;
|
|
381
|
-
let
|
|
382
|
-
let resizeObserver: ResizeObserver | null = null;
|
|
385
|
+
let observer: ReturnType<typeof observeSheetLayout> | undefined;
|
|
383
386
|
|
|
384
387
|
const attach = () => {
|
|
385
388
|
if (canceled) return;
|
|
386
|
-
const
|
|
387
|
-
|
|
389
|
+
const layoutEl = sizedLayoutRef.current;
|
|
390
|
+
const drawer = drawerContentRef.current;
|
|
391
|
+
if (!drawer || !layoutEl?.isConnected) {
|
|
388
392
|
// Radix Presence defers the portal mount; poll until the drawer is live.
|
|
389
393
|
rafId = window.requestAnimationFrame(attach);
|
|
390
394
|
return;
|
|
391
395
|
}
|
|
392
396
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const headerEl = getDOMElement(headerElRef.current);
|
|
410
|
-
|
|
411
|
-
// Skip mutations that don't change the observed topology — e.g. a
|
|
412
|
-
// virtualized list mounting rows inside the plugged scroller. Size
|
|
413
|
-
// changes are already covered by the ResizeObserver.
|
|
414
|
-
if (
|
|
415
|
-
observed &&
|
|
416
|
-
observed.contentEl === contentEl &&
|
|
417
|
-
contentEl?.isConnected &&
|
|
418
|
-
observed.headerEl === headerEl &&
|
|
419
|
-
observed.scroller.isConnected &&
|
|
420
|
-
observed.scrollContent === observed.scroller.firstElementChild
|
|
421
|
-
) {
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const resolved =
|
|
426
|
-
contentEl && contentEl.isConnected ? getScrollableElement(scrollableRef?.current) : null;
|
|
427
|
-
const scroller = resolved?.isConnected ? resolved : null;
|
|
428
|
-
detectedScrollerRef.current = scroller;
|
|
429
|
-
setHasBoundedScrollable(scroller != null);
|
|
430
|
-
observed = scroller
|
|
431
|
-
? { contentEl, headerEl, scroller, scrollContent: scroller.firstElementChild }
|
|
432
|
-
: null;
|
|
433
|
-
|
|
434
|
-
resizeObserver?.disconnect();
|
|
435
|
-
resizeObserver = new ResizeObserver(measure);
|
|
436
|
-
if (contentEl?.isConnected) resizeObserver.observe(contentEl);
|
|
437
|
-
if (headerEl) resizeObserver.observe(headerEl);
|
|
438
|
-
if (scroller) {
|
|
439
|
-
resizeObserver.observe(scroller);
|
|
440
|
-
if (scroller.firstElementChild) resizeObserver.observe(scroller.firstElementChild);
|
|
441
|
-
}
|
|
442
|
-
measure();
|
|
443
|
-
};
|
|
444
|
-
|
|
445
|
-
observe();
|
|
446
|
-
// Watch the whole drawer subtree — catches the scrollable mounting/
|
|
447
|
-
// unmounting AND the layout-branch swap, which remounts the content node
|
|
448
|
-
// (a content-scoped observer would go stale after the swap).
|
|
449
|
-
mutationObserver = new MutationObserver(observe);
|
|
450
|
-
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
|
+
});
|
|
451
413
|
};
|
|
452
414
|
|
|
453
415
|
rafId = window.requestAnimationFrame(attach);
|
|
@@ -455,10 +417,16 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
455
417
|
return () => {
|
|
456
418
|
canceled = true;
|
|
457
419
|
window.cancelAnimationFrame(rafId);
|
|
458
|
-
|
|
459
|
-
resizeObserver?.disconnect();
|
|
420
|
+
observer?.disconnect();
|
|
460
421
|
};
|
|
461
|
-
}, [
|
|
422
|
+
}, [
|
|
423
|
+
isOpen,
|
|
424
|
+
absoluteHeader,
|
|
425
|
+
absoluteFooter,
|
|
426
|
+
footerInsetAdjustment,
|
|
427
|
+
measureContent,
|
|
428
|
+
scrollableRef,
|
|
429
|
+
]);
|
|
462
430
|
|
|
463
431
|
// Below the last detent a vertical touch pan moves the sheet, not the
|
|
464
432
|
// content — `[data-vaul-scroll-locked]` disables vertical touch panning on
|
|
@@ -466,13 +434,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
466
434
|
const isScrollLocked =
|
|
467
435
|
validDetents.length > 0 && activeSnapPoint !== validDetents[validDetents.length - 1];
|
|
468
436
|
|
|
469
|
-
// Vaul measures the auto-size wrapper's offsetHeight (always, post fork).
|
|
470
|
-
// Track it here so the form sheet can size its card to fit content,
|
|
471
|
-
// clamped between a minimum ratio of the viewport and a maximum derived
|
|
472
|
-
// from `detachedOffset` (the breathing room left at top + bottom of the
|
|
473
|
-
// floating card).
|
|
474
|
-
const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
|
|
475
|
-
|
|
476
437
|
const effectiveMaxContentHeight = useMemo<number | undefined>(() => {
|
|
477
438
|
if (maxContentHeight !== undefined) return maxContentHeight;
|
|
478
439
|
if (!isFormSheet) return undefined;
|
|
@@ -539,7 +500,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
539
500
|
effectiveMaxContentHeight,
|
|
540
501
|
peekHeight,
|
|
541
502
|
peekContentHeight,
|
|
542
|
-
measuredContentHeight,
|
|
543
503
|
});
|
|
544
504
|
geometryInputsRef.current = {
|
|
545
505
|
effectiveDetached,
|
|
@@ -547,7 +507,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
547
507
|
effectiveMaxContentHeight,
|
|
548
508
|
peekHeight,
|
|
549
509
|
peekContentHeight,
|
|
550
|
-
measuredContentHeight,
|
|
551
510
|
};
|
|
552
511
|
|
|
553
512
|
const computeDetentGeometry = useCallback(() => {
|
|
@@ -561,12 +520,9 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
561
520
|
inputs.effectiveMaxContentHeight !== undefined
|
|
562
521
|
? Math.min(effectiveH, inputs.effectiveMaxContentHeight)
|
|
563
522
|
: effectiveH;
|
|
564
|
-
//
|
|
565
|
-
//
|
|
566
|
-
|
|
567
|
-
// so it reads 0 until the ResizeObserver fires). Falls back to the state
|
|
568
|
-
// value, then to vaul's pre-measure fallback (effectiveHeight / 2).
|
|
569
|
-
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;
|
|
570
526
|
const autoHeight = Math.min(measuredHeight > 0 ? measuredHeight : effectiveH / 2, ceiling);
|
|
571
527
|
|
|
572
528
|
// 'peek' is measured live from the DOM (offsetHeight is layout-based, so
|
|
@@ -602,7 +558,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
602
558
|
values.push(effectiveH > 0 ? h / effectiveH : 0);
|
|
603
559
|
}
|
|
604
560
|
return { windowH, effectiveH, ceiling, positions, values };
|
|
605
|
-
}, [
|
|
561
|
+
}, []);
|
|
606
562
|
|
|
607
563
|
// Detent info for lifecycle events. Position/detent come from the active
|
|
608
564
|
// detent's target geometry — not the live DOM rect — so willPresent (drawer
|
|
@@ -681,54 +637,17 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
681
637
|
[computeDetentGeometry]
|
|
682
638
|
);
|
|
683
639
|
|
|
684
|
-
// Mirror native synchronous layout: while dragging (and through the post-
|
|
685
|
-
// release settle) the sized layout tracks the sheet's visible height in
|
|
686
|
-
// realtime, clamped to the smallest detent — dragging below it slides the
|
|
687
|
-
// sheet out without resizing. Once settled, the inline height is cleared so
|
|
688
|
-
// the CSS calc() owns sizing again (adapts to viewport resizes at rest).
|
|
689
640
|
const sizedLayoutRef = useRef<HTMLDivElement>(null);
|
|
690
641
|
const isDraggingRef = useRef(false);
|
|
691
|
-
const isSettlingAfterDragRef = useRef(false);
|
|
692
|
-
const updateSizedLayoutHeight = useCallback(
|
|
693
|
-
(position: number) => {
|
|
694
|
-
const node = sizedLayoutRef.current;
|
|
695
|
-
if (!node) return;
|
|
696
|
-
|
|
697
|
-
const { effectiveH, ceiling, positions } = computeDetentGeometry();
|
|
698
|
-
if (positions.length === 0) return;
|
|
699
|
-
|
|
700
|
-
if (isSettlingAfterDragRef.current) {
|
|
701
|
-
const snap = activeSnapPointRef.current;
|
|
702
|
-
const index = snap != null ? validDetentsRef.current.indexOf(snap) : -1;
|
|
703
|
-
const target = index >= 0 ? positions[index]! : null;
|
|
704
|
-
if (target != null && Math.abs(position - target) < 1) {
|
|
705
|
-
isSettlingAfterDragRef.current = false;
|
|
706
|
-
// Restore the calc — height is an inline style, so clearing it would
|
|
707
|
-
// leave the div unsized (React won't re-apply it without a render)
|
|
708
|
-
node.style.height = SIZED_LAYOUT_HEIGHT;
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
// positions[0] is the smallest detent's top-Y — its height is the clamp floor.
|
|
714
|
-
const minHeight = effectiveH - positions[0]!;
|
|
715
|
-
const height = Math.min(Math.max(effectiveH - position, minHeight), ceiling);
|
|
716
|
-
node.style.height = `${height}px`;
|
|
717
|
-
},
|
|
718
|
-
[computeDetentGeometry]
|
|
719
|
-
);
|
|
720
642
|
|
|
721
643
|
const handlePositionChange = useCallback(
|
|
722
644
|
(position: number) => {
|
|
723
|
-
if (isDraggingRef.current || isSettlingAfterDragRef.current) {
|
|
724
|
-
updateSizedLayoutHeight(position);
|
|
725
|
-
}
|
|
726
645
|
const { index, detent } = interpolateFromPosition(position);
|
|
727
646
|
onPositionChangeRef.current?.({
|
|
728
647
|
nativeEvent: { index, position, detent, realtime: true },
|
|
729
648
|
} as PositionChangeEvent);
|
|
730
649
|
},
|
|
731
|
-
[interpolateFromPosition
|
|
650
|
+
[interpolateFromPosition]
|
|
732
651
|
);
|
|
733
652
|
|
|
734
653
|
// Fire onMount once after first render. React-mount is the earliest point
|
|
@@ -856,17 +775,11 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
856
775
|
}
|
|
857
776
|
onDragChangeRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragChangeEvent);
|
|
858
777
|
}, [computeDetentInfo]);
|
|
859
|
-
const handleRelease = useCallback(
|
|
860
|
-
(
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
// stays open — a dismissal slides out at the min-clamped height.
|
|
865
|
-
isSettlingAfterDragRef.current = open;
|
|
866
|
-
onDragEndRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragEndEvent);
|
|
867
|
-
},
|
|
868
|
-
[computeDetentInfo]
|
|
869
|
-
);
|
|
778
|
+
const handleRelease = useCallback(() => {
|
|
779
|
+
if (!isDraggingRef.current) return;
|
|
780
|
+
isDraggingRef.current = false;
|
|
781
|
+
onDragEndRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragEndEvent);
|
|
782
|
+
}, [computeDetentInfo]);
|
|
870
783
|
|
|
871
784
|
const { isNested, dismissAbove, descendants } = useSheetStack(
|
|
872
785
|
methodsRef,
|
|
@@ -886,6 +799,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
886
799
|
// Mirror Android: translate this sheet down to match the deepest descendant's
|
|
887
800
|
// top so the whole stack visually aligns. Cascades because every ancestor
|
|
888
801
|
// re-runs whenever the stack (and thus its descendants) changes.
|
|
802
|
+
const wasCascadedRef = useRef(false);
|
|
889
803
|
useEffect(() => {
|
|
890
804
|
const parent = drawerContentRef.current;
|
|
891
805
|
if (!parent) return;
|
|
@@ -897,7 +811,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
897
811
|
if (!isOpen) return;
|
|
898
812
|
const parentWrapper = parent.closest<HTMLElement>('[data-vaul-detached-wrapper]');
|
|
899
813
|
|
|
900
|
-
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(',')})`;
|
|
901
815
|
const wrapperTransition = `clip-path ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
902
816
|
const CLIP_NONE = 'inset(0px round 0px)';
|
|
903
817
|
|
|
@@ -921,11 +835,14 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
921
835
|
};
|
|
922
836
|
|
|
923
837
|
if (descendants.length === 0) {
|
|
838
|
+
if (!wasCascadedRef.current) return;
|
|
839
|
+
wasCascadedRef.current = false;
|
|
924
840
|
parent.style.transition = transition;
|
|
925
841
|
parent.style.transform = '';
|
|
926
842
|
setClip(CLIP_NONE);
|
|
927
843
|
return;
|
|
928
844
|
}
|
|
845
|
+
wasCascadedRef.current = true;
|
|
929
846
|
|
|
930
847
|
// Track the immediate child's *actual* top: its cascade transform when it
|
|
931
848
|
// was itself pushed behind a grandchild, else its own snap point. Mirrors
|
|
@@ -1085,11 +1002,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1085
1002
|
};
|
|
1086
1003
|
}, [isOpen, descendants.length]);
|
|
1087
1004
|
|
|
1088
|
-
// Definite-height flex layout (per-detent sizing) unless content must be
|
|
1089
|
-
// measured in natural flow: 'auto' detents (without a plugged scrollable —
|
|
1090
|
-
// those measure via `measureNaturalHeight`) and form-sheet content-fit sizing.
|
|
1091
|
-
const useSizedLayout = (!hasAutoDetent || hasBoundedScrollable) && !isFormSheet;
|
|
1092
|
-
|
|
1093
1005
|
const effectiveCornerRadius = cornerRadius ?? DEFAULT_CORNER_RADIUS;
|
|
1094
1006
|
|
|
1095
1007
|
// Shadow cast upward from the sheet's top edge toward the background. Matches
|
|
@@ -1128,13 +1040,13 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1128
1040
|
// Lift content above iOS home indicator / bottom safe area when enabled.
|
|
1129
1041
|
// A relative footer owns the inset instead (see resolvedFooterStyle); an
|
|
1130
1042
|
// absolute footer floats over the content, so the content keeps its lift.
|
|
1131
|
-
// A plugged scrollable keeps the lift too (
|
|
1132
|
-
//
|
|
1133
|
-
// with user-applied content padding.
|
|
1043
|
+
// A plugged scrollable keeps the lift too (contentInsetAdjustment
|
|
1044
|
+
// 'automatic' | 'safe-area') — 'footer' | 'never' scroll edge-to-edge
|
|
1045
|
+
// behind the indicator with user-applied content padding.
|
|
1134
1046
|
paddingBottom:
|
|
1135
1047
|
insetAdjustment === 'automatic' &&
|
|
1136
1048
|
!(footerOwnsInset && !absoluteFooter) &&
|
|
1137
|
-
(!scrollableRef ||
|
|
1049
|
+
(!scrollableRef || insetBehavior === 'automatic' || insetBehavior === 'safe-area')
|
|
1138
1050
|
? 'env(safe-area-inset-bottom, 0px)'
|
|
1139
1051
|
: 0,
|
|
1140
1052
|
}),
|
|
@@ -1145,7 +1057,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1145
1057
|
footerOwnsInset,
|
|
1146
1058
|
absoluteFooter,
|
|
1147
1059
|
scrollableRef,
|
|
1148
|
-
|
|
1060
|
+
insetBehavior,
|
|
1149
1061
|
]
|
|
1150
1062
|
);
|
|
1151
1063
|
|
|
@@ -1184,7 +1096,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1184
1096
|
// `maxContentWidth` is ignored ('form' is absolute).
|
|
1185
1097
|
// - presentation='page' → `maxContentWidth` (any viewport) or
|
|
1186
1098
|
// DEFAULT_MAX_WIDTH (tablet/landscape readability cap).
|
|
1187
|
-
// Detached without a width constraint applies
|
|
1099
|
+
// Detached without a width constraint applies placementOffset on both edges so
|
|
1188
1100
|
// the floating card breathes from the viewport sides.
|
|
1189
1101
|
const wrapperStyle = useMemo<React.CSSProperties | undefined>(() => {
|
|
1190
1102
|
// Mobile portrait ignores width sizing entirely (matches iOS/Android:
|
|
@@ -1203,30 +1115,30 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1203
1115
|
if (dropShadow) next.filter = dropShadow;
|
|
1204
1116
|
if (!needsMargins) return next;
|
|
1205
1117
|
|
|
1206
|
-
let
|
|
1207
|
-
let
|
|
1118
|
+
let marginInlineStart: number | string;
|
|
1119
|
+
let marginInlineEnd: number | string;
|
|
1208
1120
|
if (isFormSheet) {
|
|
1209
|
-
|
|
1210
|
-
|
|
1121
|
+
marginInlineStart = 'auto';
|
|
1122
|
+
marginInlineEnd = 'auto';
|
|
1211
1123
|
} else if (maxWidth == null) {
|
|
1212
|
-
|
|
1213
|
-
|
|
1124
|
+
marginInlineStart = placementOffset;
|
|
1125
|
+
marginInlineEnd = placementOffset;
|
|
1214
1126
|
} else {
|
|
1215
|
-
|
|
1216
|
-
|
|
1127
|
+
marginInlineStart = placement === 'leading' ? placementOffset : 'auto';
|
|
1128
|
+
marginInlineEnd = placement === 'trailing' ? placementOffset : 'auto';
|
|
1217
1129
|
}
|
|
1218
1130
|
|
|
1219
1131
|
if (maxWidth != null) next.maxWidth = maxWidth;
|
|
1220
|
-
next.
|
|
1221
|
-
next.
|
|
1132
|
+
next.marginInlineStart = marginInlineStart;
|
|
1133
|
+
next.marginInlineEnd = marginInlineEnd;
|
|
1222
1134
|
return next;
|
|
1223
1135
|
}, [
|
|
1224
1136
|
isLandscapeOrTablet,
|
|
1225
1137
|
isFormSheet,
|
|
1226
1138
|
maxContentWidth,
|
|
1227
1139
|
presentation,
|
|
1228
|
-
|
|
1229
|
-
|
|
1140
|
+
placement,
|
|
1141
|
+
placementOffset,
|
|
1230
1142
|
effectiveDetached,
|
|
1231
1143
|
dropShadow,
|
|
1232
1144
|
]);
|
|
@@ -1275,14 +1187,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1275
1187
|
peekHeight={peekHeight}
|
|
1276
1188
|
initialAnimated={initialDetentAnimated}
|
|
1277
1189
|
detachedWrapperStyle={wrapperStyle}
|
|
1278
|
-
|
|
1279
|
-
contentHeight={
|
|
1280
|
-
// Bounded scrollable → the auto-size wrapper measures 0 (sized
|
|
1281
|
-
// layout is absolute), so feed vaul the natural height instead.
|
|
1282
|
-
hasAutoDetent && hasBoundedScrollable && scrollableAutoHeight > 0
|
|
1283
|
-
? scrollableAutoHeight
|
|
1284
|
-
: undefined
|
|
1285
|
-
}
|
|
1190
|
+
contentHeight={measuredContentHeight}
|
|
1286
1191
|
activeSnapPoint={activeSnapPoint}
|
|
1287
1192
|
setActiveSnapPoint={handleSetActiveSnapPoint}
|
|
1288
1193
|
{...snapPointsProps}
|
|
@@ -1307,64 +1212,29 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
1307
1212
|
{accessibilityOptions?.paneTitle ?? 'Sheet'}
|
|
1308
1213
|
</Drawer.Title>
|
|
1309
1214
|
{grabber && <Drawer.Handle style={handleStyle} />}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
{isValidElement(header) ? header : createElement(header)}
|
|
1325
|
-
</View>
|
|
1326
|
-
)}
|
|
1327
|
-
{/* Content lays out naturally like native — fill only for auto
|
|
1328
|
-
detents with a plugged scrollable, where natural layout is
|
|
1329
|
-
circular (sheet height derives from the scroll content size).
|
|
1330
|
-
Unlike native, CSS won't cap the content to the container
|
|
1331
|
-
(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 && (
|
|
1332
1229
|
<View
|
|
1333
|
-
ref={
|
|
1334
|
-
style={
|
|
1335
|
-
onLayout={
|
|
1230
|
+
ref={footerElRef}
|
|
1231
|
+
style={[relativeFooterStyle, resolvedFooterStyle]}
|
|
1232
|
+
onLayout={handleFooterLayout}
|
|
1336
1233
|
>
|
|
1337
|
-
{
|
|
1234
|
+
{isValidElement(footer) ? footer : createElement(footer)}
|
|
1338
1235
|
</View>
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
ref={footerElRef}
|
|
1342
|
-
style={[relativeFooterStyle, resolvedFooterStyle]}
|
|
1343
|
-
onLayout={handleFooterLayout}
|
|
1344
|
-
>
|
|
1345
|
-
{isValidElement(footer) ? footer : createElement(footer)}
|
|
1346
|
-
</View>
|
|
1347
|
-
)}
|
|
1348
|
-
</div>
|
|
1349
|
-
) : (
|
|
1350
|
-
// Natural flow so vaul can measure content height — required for
|
|
1351
|
-
// 'auto' detents and form-sheet content-fit sizing.
|
|
1352
|
-
<>
|
|
1353
|
-
{header && (
|
|
1354
|
-
<View ref={headerElRef} style={resolvedHeaderStyle} onLayout={handleHeaderLayout}>
|
|
1355
|
-
{isValidElement(header) ? header : createElement(header)}
|
|
1356
|
-
</View>
|
|
1357
|
-
)}
|
|
1358
|
-
<View ref={contentRef} style={style} onLayout={handleContentLayout}>
|
|
1359
|
-
{children}
|
|
1360
|
-
</View>
|
|
1361
|
-
{footer && !absoluteFooter && (
|
|
1362
|
-
<View ref={footerElRef} style={resolvedFooterStyle} onLayout={handleFooterLayout}>
|
|
1363
|
-
{isValidElement(footer) ? footer : createElement(footer)}
|
|
1364
|
-
</View>
|
|
1365
|
-
)}
|
|
1366
|
-
</>
|
|
1367
|
-
)}
|
|
1236
|
+
)}
|
|
1237
|
+
</div>
|
|
1368
1238
|
</Drawer.Content>
|
|
1369
1239
|
</Drawer.Portal>
|
|
1370
1240
|
</Drawer.Root>
|
|
@@ -1402,12 +1272,11 @@ const sizedLayoutStyle: React.CSSProperties = {
|
|
|
1402
1272
|
height: SIZED_LAYOUT_HEIGHT,
|
|
1403
1273
|
display: 'flex',
|
|
1404
1274
|
flexDirection: 'column',
|
|
1275
|
+
boxSizing: 'border-box',
|
|
1276
|
+
paddingBottom: 'inherit',
|
|
1405
1277
|
};
|
|
1406
1278
|
|
|
1407
|
-
const
|
|
1408
|
-
flex: 1,
|
|
1409
|
-
minHeight: 0,
|
|
1410
|
-
} as const;
|
|
1279
|
+
const contentStyle = { flexShrink: 1, minHeight: 0 } as const;
|
|
1411
1280
|
|
|
1412
1281
|
const visuallyHiddenStyle: React.CSSProperties = {
|
|
1413
1282
|
position: 'absolute',
|