@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
|
@@ -5,8 +5,9 @@ import { createElement, forwardRef, isValidElement, useCallback, useEffect, useI
|
|
|
5
5
|
import { StyleSheet, useColorScheme, useWindowDimensions, View } from 'react-native';
|
|
6
6
|
import { measurePeekContentHeight, TrueSheetPeekContext } from "./TrueSheetPeek.web.js";
|
|
7
7
|
import { usePortalContainer, useRegisterSheet, useSheetStack } from "./TrueSheetProvider.web.js";
|
|
8
|
-
import { COLOR_SURFACE_CONTAINER_LOW_DARK, COLOR_SURFACE_CONTAINER_LOW_LIGHT,
|
|
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;
|
|
@@ -32,8 +33,8 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
32
33
|
backgroundColor: backgroundColorProp,
|
|
33
34
|
maxContentHeight,
|
|
34
35
|
maxContentWidth,
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
placement = 'automatic',
|
|
37
|
+
placementOffset = DEFAULT_PLACEMENT_OFFSET,
|
|
37
38
|
scrollableRef,
|
|
38
39
|
scrollableOptions,
|
|
39
40
|
grabber = true,
|
|
@@ -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
|
}, []);
|
|
@@ -201,16 +201,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
201
201
|
const [peekContentHeight, setPeekContentHeight] = useState(0);
|
|
202
202
|
const contentRef = useRef(null);
|
|
203
203
|
const peekElRef = useRef(null);
|
|
204
|
-
const
|
|
205
|
-
contentRef,
|
|
206
|
-
peekRef: peekElRef,
|
|
207
|
-
setPeekContentHeight
|
|
208
|
-
}), []);
|
|
209
|
-
|
|
210
|
-
// The peek's own onLayout only fires when *it* resizes. Content added above
|
|
211
|
-
// it moves it without resizing it, so re-measure its offset whenever the
|
|
212
|
-
// content view's layout changes.
|
|
213
|
-
const handleContentLayout = useCallback(() => {
|
|
204
|
+
const measurePeek = useCallback(() => {
|
|
214
205
|
const peekEl = getDOMElement(peekElRef.current);
|
|
215
206
|
const contentEl = getDOMElement(contentRef.current);
|
|
216
207
|
if (peekEl && contentEl) {
|
|
@@ -218,11 +209,32 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
218
209
|
}
|
|
219
210
|
}, []);
|
|
220
211
|
|
|
212
|
+
// Last-attached wins — during a screen swap the incoming peek mounts before
|
|
213
|
+
// the outgoing one unmounts, so the outgoing detach must not clear it.
|
|
214
|
+
const peekContext = useMemo(() => ({
|
|
215
|
+
contentRef,
|
|
216
|
+
attachPeek: view => {
|
|
217
|
+
peekElRef.current = view;
|
|
218
|
+
measurePeek();
|
|
219
|
+
},
|
|
220
|
+
detachPeek: view => {
|
|
221
|
+
if (peekElRef.current !== view) return;
|
|
222
|
+
peekElRef.current = null;
|
|
223
|
+
setPeekContentHeight(0);
|
|
224
|
+
},
|
|
225
|
+
measurePeek: view => {
|
|
226
|
+
if (peekElRef.current === view) measurePeek();
|
|
227
|
+
}
|
|
228
|
+
}), [measurePeek]);
|
|
229
|
+
|
|
230
|
+
// The peek's own onLayout only fires when *it* resizes. Content added above
|
|
231
|
+
// it moves it without resizing it, so re-measure its offset whenever the
|
|
232
|
+
// content view's layout changes.
|
|
233
|
+
const handleContentLayout = measurePeek;
|
|
234
|
+
|
|
221
235
|
// An absolute (floating) header overlaps the content, so it contributes no
|
|
222
236
|
// height to the 'auto' detent measurement.
|
|
223
237
|
const absoluteHeader = headerOptions?.position === 'absolute';
|
|
224
|
-
const absoluteHeaderRef = useRef(absoluteHeader);
|
|
225
|
-
absoluteHeaderRef.current = absoluteHeader;
|
|
226
238
|
const resolvedHeaderStyle = absoluteHeader ? [absoluteHeaderStyle, headerStyle] : headerStyle;
|
|
227
239
|
|
|
228
240
|
// Same for an absolute (floating) footer — rendered via vaul's
|
|
@@ -249,114 +261,54 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
249
261
|
// A relative footer is laid out below the content, so it's pushed off-screen
|
|
250
262
|
// at the peek detent and contributes no height.
|
|
251
263
|
const peekHeight = (header ? headerHeight : 0) + (footer && absoluteFooter ? footerHeight : 0) + peekContentHeight || DEFAULT_PEEK_HEIGHT;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
// `naturalHeight`.
|
|
258
|
-
const [hasBoundedScrollable, setHasBoundedScrollable] = useState(false);
|
|
259
|
-
const [scrollableAutoHeight, setScrollableAutoHeight] = useState(0);
|
|
260
|
-
const detectedScrollerRef = useRef(null);
|
|
261
|
-
|
|
262
|
-
// Natural content height (header + content + footer, with a detected
|
|
263
|
-
// scrollable's viewport replaced by its content size) — the height the
|
|
264
|
-
// content wants regardless of the sheet's bounds.
|
|
265
|
-
const measureNaturalHeight = useCallback(() => {
|
|
266
|
-
const contentEl = getDOMElement(contentRef.current);
|
|
267
|
-
if (!contentEl || !contentEl.isConnected) return 0;
|
|
268
|
-
const headerEl = absoluteHeaderRef.current ? null : getDOMElement(headerElRef.current);
|
|
269
|
-
const footerEl = absoluteFooterRef.current ? null : getDOMElement(footerElRef.current);
|
|
270
|
-
let height = (headerEl?.offsetHeight ?? 0) + (footerEl?.offsetHeight ?? 0) + contentEl.offsetHeight;
|
|
271
|
-
const scroller = detectedScrollerRef.current;
|
|
272
|
-
const scrollContent = scroller?.firstElementChild;
|
|
273
|
-
if (scroller?.isConnected && scrollContent instanceof HTMLElement) {
|
|
274
|
-
height += scrollContent.offsetHeight - scroller.clientHeight;
|
|
275
|
-
}
|
|
276
|
-
return Math.max(0, height);
|
|
277
|
-
}, []);
|
|
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;
|
|
278
269
|
useEffect(() => {
|
|
279
|
-
if (!isOpen
|
|
270
|
+
if (!isOpen) return undefined;
|
|
280
271
|
let canceled = false;
|
|
281
272
|
let rafId = 0;
|
|
282
|
-
let
|
|
283
|
-
let resizeObserver = null;
|
|
273
|
+
let observer;
|
|
284
274
|
const attach = () => {
|
|
285
275
|
if (canceled) return;
|
|
286
|
-
const
|
|
287
|
-
|
|
276
|
+
const layoutEl = sizedLayoutRef.current;
|
|
277
|
+
const drawer = drawerContentRef.current;
|
|
278
|
+
if (!drawer || !layoutEl?.isConnected) {
|
|
288
279
|
// Radix Presence defers the portal mount; poll until the drawer is live.
|
|
289
280
|
rafId = window.requestAnimationFrame(attach);
|
|
290
281
|
return;
|
|
291
282
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
// changes are already covered by the ResizeObserver.
|
|
307
|
-
if (observed && observed.contentEl === contentEl && contentEl?.isConnected && observed.headerEl === headerEl && observed.scroller.isConnected && observed.scrollContent === observed.scroller.firstElementChild) {
|
|
308
|
-
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);
|
|
309
297
|
}
|
|
310
|
-
const resolved = contentEl && contentEl.isConnected ? getScrollableElement(scrollableRef?.current) : null;
|
|
311
|
-
const scroller = resolved?.isConnected ? resolved : null;
|
|
312
|
-
detectedScrollerRef.current = scroller;
|
|
313
|
-
setHasBoundedScrollable(scroller != null);
|
|
314
|
-
observed = scroller ? {
|
|
315
|
-
contentEl,
|
|
316
|
-
headerEl,
|
|
317
|
-
scroller,
|
|
318
|
-
scrollContent: scroller.firstElementChild
|
|
319
|
-
} : null;
|
|
320
|
-
resizeObserver?.disconnect();
|
|
321
|
-
resizeObserver = new ResizeObserver(measure);
|
|
322
|
-
if (contentEl?.isConnected) resizeObserver.observe(contentEl);
|
|
323
|
-
if (headerEl) resizeObserver.observe(headerEl);
|
|
324
|
-
if (scroller) {
|
|
325
|
-
resizeObserver.observe(scroller);
|
|
326
|
-
if (scroller.firstElementChild) resizeObserver.observe(scroller.firstElementChild);
|
|
327
|
-
}
|
|
328
|
-
measure();
|
|
329
|
-
};
|
|
330
|
-
observe();
|
|
331
|
-
// Watch the whole drawer subtree — catches the scrollable mounting/
|
|
332
|
-
// unmounting AND the layout-branch swap, which remounts the content node
|
|
333
|
-
// (a content-scoped observer would go stale after the swap).
|
|
334
|
-
mutationObserver = new MutationObserver(observe);
|
|
335
|
-
mutationObserver.observe(drawerEl, {
|
|
336
|
-
childList: true,
|
|
337
|
-
subtree: true
|
|
338
298
|
});
|
|
339
299
|
};
|
|
340
300
|
rafId = window.requestAnimationFrame(attach);
|
|
341
301
|
return () => {
|
|
342
302
|
canceled = true;
|
|
343
303
|
window.cancelAnimationFrame(rafId);
|
|
344
|
-
|
|
345
|
-
resizeObserver?.disconnect();
|
|
304
|
+
observer?.disconnect();
|
|
346
305
|
};
|
|
347
|
-
}, [isOpen,
|
|
306
|
+
}, [isOpen, absoluteHeader, absoluteFooter, footerInsetAdjustment, measureContent, scrollableRef]);
|
|
348
307
|
|
|
349
308
|
// Below the last detent a vertical touch pan moves the sheet, not the
|
|
350
309
|
// content — `[data-vaul-scroll-locked]` disables vertical touch panning on
|
|
351
310
|
// the scroll container and everything inside it (see vaul/style.css).
|
|
352
311
|
const isScrollLocked = validDetents.length > 0 && activeSnapPoint !== validDetents[validDetents.length - 1];
|
|
353
|
-
|
|
354
|
-
// Vaul measures the auto-size wrapper's offsetHeight (always, post fork).
|
|
355
|
-
// Track it here so the form sheet can size its card to fit content,
|
|
356
|
-
// clamped between a minimum ratio of the viewport and a maximum derived
|
|
357
|
-
// from `detachedOffset` (the breathing room left at top + bottom of the
|
|
358
|
-
// floating card).
|
|
359
|
-
const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
|
|
360
312
|
const effectiveMaxContentHeight = useMemo(() => {
|
|
361
313
|
if (maxContentHeight !== undefined) return maxContentHeight;
|
|
362
314
|
if (!isFormSheet) return undefined;
|
|
@@ -419,16 +371,14 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
419
371
|
effectiveDetachedOffset,
|
|
420
372
|
effectiveMaxContentHeight,
|
|
421
373
|
peekHeight,
|
|
422
|
-
peekContentHeight
|
|
423
|
-
measuredContentHeight
|
|
374
|
+
peekContentHeight
|
|
424
375
|
});
|
|
425
376
|
geometryInputsRef.current = {
|
|
426
377
|
effectiveDetached,
|
|
427
378
|
effectiveDetachedOffset,
|
|
428
379
|
effectiveMaxContentHeight,
|
|
429
380
|
peekHeight,
|
|
430
|
-
peekContentHeight
|
|
431
|
-
measuredContentHeight
|
|
381
|
+
peekContentHeight
|
|
432
382
|
};
|
|
433
383
|
const computeDetentGeometry = useCallback(() => {
|
|
434
384
|
const inputs = geometryInputsRef.current;
|
|
@@ -436,12 +386,9 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
436
386
|
const effectiveH = inputs.effectiveDetached ? windowH - inputs.effectiveDetachedOffset : windowH;
|
|
437
387
|
// Matches vaul's height ceiling: min(effectiveH, maxContentHeight).
|
|
438
388
|
const ceiling = inputs.effectiveMaxContentHeight !== undefined ? Math.min(effectiveH, inputs.effectiveMaxContentHeight) : effectiveH;
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
|
|
442
|
-
// so it reads 0 until the ResizeObserver fires). Falls back to the state
|
|
443
|
-
// value, then to vaul's pre-measure fallback (effectiveHeight / 2).
|
|
444
|
-
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;
|
|
445
392
|
const autoHeight = Math.min(measuredHeight > 0 ? measuredHeight : effectiveH / 2, ceiling);
|
|
446
393
|
|
|
447
394
|
// 'peek' is measured live from the DOM (offsetHeight is layout-based, so
|
|
@@ -472,7 +419,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
472
419
|
positions,
|
|
473
420
|
values
|
|
474
421
|
};
|
|
475
|
-
}, [
|
|
422
|
+
}, []);
|
|
476
423
|
|
|
477
424
|
// Detent info for lifecycle events. Position/detent come from the active
|
|
478
425
|
// detent's target geometry — not the live DOM rect — so willPresent (drawer
|
|
@@ -559,46 +506,9 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
559
506
|
detent: values[count - 1]
|
|
560
507
|
};
|
|
561
508
|
}, [computeDetentGeometry]);
|
|
562
|
-
|
|
563
|
-
// Mirror native synchronous layout: while dragging (and through the post-
|
|
564
|
-
// release settle) the sized layout tracks the sheet's visible height in
|
|
565
|
-
// realtime, clamped to the smallest detent — dragging below it slides the
|
|
566
|
-
// sheet out without resizing. Once settled, the inline height is cleared so
|
|
567
|
-
// the CSS calc() owns sizing again (adapts to viewport resizes at rest).
|
|
568
509
|
const sizedLayoutRef = useRef(null);
|
|
569
510
|
const isDraggingRef = useRef(false);
|
|
570
|
-
const isSettlingAfterDragRef = useRef(false);
|
|
571
|
-
const updateSizedLayoutHeight = useCallback(position => {
|
|
572
|
-
const node = sizedLayoutRef.current;
|
|
573
|
-
if (!node) return;
|
|
574
|
-
const {
|
|
575
|
-
effectiveH,
|
|
576
|
-
ceiling,
|
|
577
|
-
positions
|
|
578
|
-
} = computeDetentGeometry();
|
|
579
|
-
if (positions.length === 0) return;
|
|
580
|
-
if (isSettlingAfterDragRef.current) {
|
|
581
|
-
const snap = activeSnapPointRef.current;
|
|
582
|
-
const index = snap != null ? validDetentsRef.current.indexOf(snap) : -1;
|
|
583
|
-
const target = index >= 0 ? positions[index] : null;
|
|
584
|
-
if (target != null && Math.abs(position - target) < 1) {
|
|
585
|
-
isSettlingAfterDragRef.current = false;
|
|
586
|
-
// Restore the calc — height is an inline style, so clearing it would
|
|
587
|
-
// leave the div unsized (React won't re-apply it without a render)
|
|
588
|
-
node.style.height = SIZED_LAYOUT_HEIGHT;
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// positions[0] is the smallest detent's top-Y — its height is the clamp floor.
|
|
594
|
-
const minHeight = effectiveH - positions[0];
|
|
595
|
-
const height = Math.min(Math.max(effectiveH - position, minHeight), ceiling);
|
|
596
|
-
node.style.height = `${height}px`;
|
|
597
|
-
}, [computeDetentGeometry]);
|
|
598
511
|
const handlePositionChange = useCallback(position => {
|
|
599
|
-
if (isDraggingRef.current || isSettlingAfterDragRef.current) {
|
|
600
|
-
updateSizedLayoutHeight(position);
|
|
601
|
-
}
|
|
602
512
|
const {
|
|
603
513
|
index,
|
|
604
514
|
detent
|
|
@@ -611,7 +521,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
611
521
|
realtime: true
|
|
612
522
|
}
|
|
613
523
|
});
|
|
614
|
-
}, [interpolateFromPosition
|
|
524
|
+
}, [interpolateFromPosition]);
|
|
615
525
|
|
|
616
526
|
// Fire onMount once after first render. React-mount is the earliest point
|
|
617
527
|
// the component is ready for imperative calls, matching the native
|
|
@@ -757,12 +667,9 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
757
667
|
nativeEvent: computeDetentInfo(true)
|
|
758
668
|
});
|
|
759
669
|
}, [computeDetentInfo]);
|
|
760
|
-
const handleRelease = useCallback((
|
|
670
|
+
const handleRelease = useCallback(() => {
|
|
761
671
|
if (!isDraggingRef.current) return;
|
|
762
672
|
isDraggingRef.current = false;
|
|
763
|
-
// Track the sized layout through the settle animation only when the sheet
|
|
764
|
-
// stays open — a dismissal slides out at the min-clamped height.
|
|
765
|
-
isSettlingAfterDragRef.current = open;
|
|
766
673
|
onDragEndRef.current?.({
|
|
767
674
|
nativeEvent: computeDetentInfo(true)
|
|
768
675
|
});
|
|
@@ -784,6 +691,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
784
691
|
// Mirror Android: translate this sheet down to match the deepest descendant's
|
|
785
692
|
// top so the whole stack visually aligns. Cascades because every ancestor
|
|
786
693
|
// re-runs whenever the stack (and thus its descendants) changes.
|
|
694
|
+
const wasCascadedRef = useRef(false);
|
|
787
695
|
useEffect(() => {
|
|
788
696
|
const parent = drawerContentRef.current;
|
|
789
697
|
if (!parent) return;
|
|
@@ -794,7 +702,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
794
702
|
// out — nothing to align with anymore.
|
|
795
703
|
if (!isOpen) return;
|
|
796
704
|
const parentWrapper = parent.closest('[data-vaul-detached-wrapper]');
|
|
797
|
-
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(',')})`;
|
|
798
706
|
const wrapperTransition = `clip-path ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
799
707
|
const CLIP_NONE = 'inset(0px round 0px)';
|
|
800
708
|
|
|
@@ -817,11 +725,14 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
817
725
|
parentWrapper.style.clipPath = next;
|
|
818
726
|
};
|
|
819
727
|
if (descendants.length === 0) {
|
|
728
|
+
if (!wasCascadedRef.current) return;
|
|
729
|
+
wasCascadedRef.current = false;
|
|
820
730
|
parent.style.transition = transition;
|
|
821
731
|
parent.style.transform = '';
|
|
822
732
|
setClip(CLIP_NONE);
|
|
823
733
|
return;
|
|
824
734
|
}
|
|
735
|
+
wasCascadedRef.current = true;
|
|
825
736
|
|
|
826
737
|
// Track the immediate child's *actual* top: its cascade transform when it
|
|
827
738
|
// was itself pushed behind a grandchild, else its own snap point. Mirrors
|
|
@@ -985,11 +896,6 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
985
896
|
window.cancelAnimationFrame(rafId);
|
|
986
897
|
};
|
|
987
898
|
}, [isOpen, descendants.length]);
|
|
988
|
-
|
|
989
|
-
// Definite-height flex layout (per-detent sizing) unless content must be
|
|
990
|
-
// measured in natural flow: 'auto' detents (without a plugged scrollable —
|
|
991
|
-
// those measure via `measureNaturalHeight`) and form-sheet content-fit sizing.
|
|
992
|
-
const useSizedLayout = (!hasAutoDetent || hasBoundedScrollable) && !isFormSheet;
|
|
993
899
|
const effectiveCornerRadius = cornerRadius ?? DEFAULT_CORNER_RADIUS;
|
|
994
900
|
|
|
995
901
|
// Shadow cast upward from the sheet's top edge toward the background. Matches
|
|
@@ -1023,11 +929,11 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
1023
929
|
// Lift content above iOS home indicator / bottom safe area when enabled.
|
|
1024
930
|
// A relative footer owns the inset instead (see resolvedFooterStyle); an
|
|
1025
931
|
// absolute footer floats over the content, so the content keeps its lift.
|
|
1026
|
-
// A plugged scrollable keeps the lift too (
|
|
1027
|
-
//
|
|
1028
|
-
// with user-applied content padding.
|
|
1029
|
-
paddingBottom: insetAdjustment === 'automatic' && !(footerOwnsInset && !absoluteFooter) && (!scrollableRef ||
|
|
1030
|
-
}), [backgroundColor, effectiveCornerRadius, insetAdjustment, footerOwnsInset, absoluteFooter, scrollableRef,
|
|
932
|
+
// A plugged scrollable keeps the lift too (contentInsetAdjustment
|
|
933
|
+
// 'automatic' | 'safe-area') — 'footer' | 'never' scroll edge-to-edge
|
|
934
|
+
// behind the indicator with user-applied content padding.
|
|
935
|
+
paddingBottom: insetAdjustment === 'automatic' && !(footerOwnsInset && !absoluteFooter) && (!scrollableRef || insetBehavior === 'automatic' || insetBehavior === 'safe-area') ? 'env(safe-area-inset-bottom, 0px)' : 0
|
|
936
|
+
}), [backgroundColor, effectiveCornerRadius, insetAdjustment, footerOwnsInset, absoluteFooter, scrollableRef, insetBehavior]);
|
|
1031
937
|
const defaultGrabberColor = colorScheme === 'dark' ? DEFAULT_GRABBER_COLOR_DARK : DEFAULT_GRABBER_COLOR_LIGHT;
|
|
1032
938
|
const grabberHeight = grabberOptions?.height ?? DEFAULT_GRABBER_HEIGHT;
|
|
1033
939
|
|
|
@@ -1058,7 +964,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
1058
964
|
// `maxContentWidth` is ignored ('form' is absolute).
|
|
1059
965
|
// - presentation='page' → `maxContentWidth` (any viewport) or
|
|
1060
966
|
// DEFAULT_MAX_WIDTH (tablet/landscape readability cap).
|
|
1061
|
-
// Detached without a width constraint applies
|
|
967
|
+
// Detached without a width constraint applies placementOffset on both edges so
|
|
1062
968
|
// the floating card breathes from the viewport sides.
|
|
1063
969
|
const wrapperStyle = useMemo(() => {
|
|
1064
970
|
// Mobile portrait ignores width sizing entirely (matches iOS/Android:
|
|
@@ -1070,23 +976,23 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
1070
976
|
const next = {};
|
|
1071
977
|
if (dropShadow) next.filter = dropShadow;
|
|
1072
978
|
if (!needsMargins) return next;
|
|
1073
|
-
let
|
|
1074
|
-
let
|
|
979
|
+
let marginInlineStart;
|
|
980
|
+
let marginInlineEnd;
|
|
1075
981
|
if (isFormSheet) {
|
|
1076
|
-
|
|
1077
|
-
|
|
982
|
+
marginInlineStart = 'auto';
|
|
983
|
+
marginInlineEnd = 'auto';
|
|
1078
984
|
} else if (maxWidth == null) {
|
|
1079
|
-
|
|
1080
|
-
|
|
985
|
+
marginInlineStart = placementOffset;
|
|
986
|
+
marginInlineEnd = placementOffset;
|
|
1081
987
|
} else {
|
|
1082
|
-
|
|
1083
|
-
|
|
988
|
+
marginInlineStart = placement === 'leading' ? placementOffset : 'auto';
|
|
989
|
+
marginInlineEnd = placement === 'trailing' ? placementOffset : 'auto';
|
|
1084
990
|
}
|
|
1085
991
|
if (maxWidth != null) next.maxWidth = maxWidth;
|
|
1086
|
-
next.
|
|
1087
|
-
next.
|
|
992
|
+
next.marginInlineStart = marginInlineStart;
|
|
993
|
+
next.marginInlineEnd = marginInlineEnd;
|
|
1088
994
|
return next;
|
|
1089
|
-
}, [isLandscapeOrTablet, isFormSheet, maxContentWidth, presentation,
|
|
995
|
+
}, [isLandscapeOrTablet, isFormSheet, maxContentWidth, presentation, placement, placementOffset, effectiveDetached, dropShadow]);
|
|
1090
996
|
|
|
1091
997
|
// Absolute-position the grabber so it overlays the content top-edge
|
|
1092
998
|
// instead of consuming flow height — mirrors native iOS/Android, where
|
|
@@ -1128,11 +1034,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
1128
1034
|
peekHeight: peekHeight,
|
|
1129
1035
|
initialAnimated: initialDetentAnimated,
|
|
1130
1036
|
detachedWrapperStyle: wrapperStyle,
|
|
1131
|
-
|
|
1132
|
-
contentHeight:
|
|
1133
|
-
// Bounded scrollable → the auto-size wrapper measures 0 (sized
|
|
1134
|
-
// layout is absolute), so feed vaul the natural height instead.
|
|
1135
|
-
hasAutoDetent && hasBoundedScrollable && scrollableAutoHeight > 0 ? scrollableAutoHeight : undefined,
|
|
1037
|
+
contentHeight: measuredContentHeight,
|
|
1136
1038
|
activeSnapPoint: activeSnapPoint,
|
|
1137
1039
|
setActiveSnapPoint: handleSetActiveSnapPoint,
|
|
1138
1040
|
...snapPointsProps,
|
|
@@ -1158,15 +1060,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
1158
1060
|
children: accessibilityOptions?.paneTitle ?? 'Sheet'
|
|
1159
1061
|
}), grabber && /*#__PURE__*/_jsx(Drawer.Handle, {
|
|
1160
1062
|
style: handleStyle
|
|
1161
|
-
}),
|
|
1162
|
-
/*#__PURE__*/
|
|
1163
|
-
// vaul wraps children in `[data-vaul-auto-size-wrapper]` (display:
|
|
1164
|
-
// flow-root) which doesn't honor descendant flex layout. Use an
|
|
1165
|
-
// absolute fill sized to the visible portion (via vaul's
|
|
1166
|
-
// `--snap-point-height` var) so the inner flex column has a
|
|
1167
|
-
// definite height for flex layouts to fill — mirrors native, where
|
|
1168
|
-
// the container is sized to the sheet's visible height per detent.
|
|
1169
|
-
_jsxs("div", {
|
|
1063
|
+
}), /*#__PURE__*/_jsxs("div", {
|
|
1170
1064
|
ref: sizedLayoutRef,
|
|
1171
1065
|
style: sizedLayoutStyle,
|
|
1172
1066
|
"data-vaul-scroll-locked": isScrollLocked ? '' : undefined,
|
|
@@ -1177,7 +1071,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
1177
1071
|
children: /*#__PURE__*/isValidElement(header) ? header : /*#__PURE__*/createElement(header)
|
|
1178
1072
|
}), /*#__PURE__*/_jsx(View, {
|
|
1179
1073
|
ref: contentRef,
|
|
1180
|
-
style:
|
|
1074
|
+
style: [contentStyle, style],
|
|
1181
1075
|
onLayout: handleContentLayout,
|
|
1182
1076
|
children: children
|
|
1183
1077
|
}), footer && !absoluteFooter && /*#__PURE__*/_jsx(View, {
|
|
@@ -1186,27 +1080,6 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
1186
1080
|
onLayout: handleFooterLayout,
|
|
1187
1081
|
children: /*#__PURE__*/isValidElement(footer) ? footer : /*#__PURE__*/createElement(footer)
|
|
1188
1082
|
})]
|
|
1189
|
-
}) :
|
|
1190
|
-
/*#__PURE__*/
|
|
1191
|
-
// Natural flow so vaul can measure content height — required for
|
|
1192
|
-
// 'auto' detents and form-sheet content-fit sizing.
|
|
1193
|
-
_jsxs(_Fragment, {
|
|
1194
|
-
children: [header && /*#__PURE__*/_jsx(View, {
|
|
1195
|
-
ref: headerElRef,
|
|
1196
|
-
style: resolvedHeaderStyle,
|
|
1197
|
-
onLayout: handleHeaderLayout,
|
|
1198
|
-
children: /*#__PURE__*/isValidElement(header) ? header : /*#__PURE__*/createElement(header)
|
|
1199
|
-
}), /*#__PURE__*/_jsx(View, {
|
|
1200
|
-
ref: contentRef,
|
|
1201
|
-
style: style,
|
|
1202
|
-
onLayout: handleContentLayout,
|
|
1203
|
-
children: children
|
|
1204
|
-
}), footer && !absoluteFooter && /*#__PURE__*/_jsx(View, {
|
|
1205
|
-
ref: footerElRef,
|
|
1206
|
-
style: resolvedFooterStyle,
|
|
1207
|
-
onLayout: handleFooterLayout,
|
|
1208
|
-
children: /*#__PURE__*/isValidElement(footer) ? footer : /*#__PURE__*/createElement(footer)
|
|
1209
|
-
})]
|
|
1210
1083
|
})]
|
|
1211
1084
|
})]
|
|
1212
1085
|
})
|
|
@@ -1240,10 +1113,12 @@ const sizedLayoutStyle = {
|
|
|
1240
1113
|
right: 0,
|
|
1241
1114
|
height: SIZED_LAYOUT_HEIGHT,
|
|
1242
1115
|
display: 'flex',
|
|
1243
|
-
flexDirection: 'column'
|
|
1116
|
+
flexDirection: 'column',
|
|
1117
|
+
boxSizing: 'border-box',
|
|
1118
|
+
paddingBottom: 'inherit'
|
|
1244
1119
|
};
|
|
1245
|
-
const
|
|
1246
|
-
|
|
1120
|
+
const contentStyle = {
|
|
1121
|
+
flexShrink: 1,
|
|
1247
1122
|
minHeight: 0
|
|
1248
1123
|
};
|
|
1249
1124
|
const visuallyHiddenStyle = {
|