@lodev09/react-native-true-sheet 3.11.10 → 4.0.0-beta.1
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/README.md +6 -5
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +23 -2
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +116 -26
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +7 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +48 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterViewManager.kt +7 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +45 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +28 -12
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +99 -34
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +9 -5
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +12 -3
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +33 -4
- package/android/src/main/jni/TrueSheetSpec.h +2 -0
- package/android/src/main/jni/TrueSheetStateUpdater.cpp +96 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +24 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +43 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +28 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +11 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +37 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h +24 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +56 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +28 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +11 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +41 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.cpp +18 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.h +16 -0
- package/ios/TrueSheetContainerView.h +12 -10
- package/ios/TrueSheetContainerView.mm +11 -29
- package/ios/TrueSheetContentView.h +22 -7
- package/ios/TrueSheetContentView.mm +143 -41
- package/ios/TrueSheetFooterView.h +12 -2
- package/ios/TrueSheetFooterView.mm +74 -71
- package/ios/TrueSheetHeaderView.mm +0 -1
- package/ios/TrueSheetView.mm +60 -27
- package/ios/TrueSheetViewController.h +9 -0
- package/ios/TrueSheetViewController.mm +288 -122
- package/ios/core/TrueSheetDetentCalculator.h +9 -0
- package/ios/core/TrueSheetDetentCalculator.mm +13 -4
- package/ios/tvos/TrueSheetStubs.mm +2 -0
- package/lib/module/TrueSheet.js +32 -13
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +262 -38
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +5 -1
- package/lib/module/fabric/TrueSheetFooterViewNativeComponent.ts +10 -2
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +8 -1
- package/lib/module/web/vaul/index.js +17 -6
- package/lib/module/web/vaul/index.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +35 -11
- 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/fabric/TrueSheetContentViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts +2 -0
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +5 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/navigation/types.d.ts +1 -1
- package/lib/typescript/src/navigation/types.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/index.d.ts +8 -1
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
- package/package.json +10 -10
- package/react-native.config.js +5 -1
- package/src/TrueSheet.tsx +45 -14
- package/src/TrueSheet.types.ts +38 -12
- package/src/TrueSheet.web.tsx +314 -44
- package/src/fabric/TrueSheetContentViewNativeComponent.ts +5 -1
- package/src/fabric/TrueSheetFooterViewNativeComponent.ts +10 -2
- package/src/fabric/TrueSheetViewNativeComponent.ts +8 -1
- package/src/navigation/types.ts +3 -1
- package/src/web/vaul/index.tsx +23 -5
- package/ios/utils/LayoutUtil.h +0 -77
- package/ios/utils/LayoutUtil.mm +0 -106
|
@@ -2,13 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
/// <reference lib="dom" />
|
|
4
4
|
import { createElement, forwardRef, isValidElement, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
5
|
-
import { useColorScheme, useWindowDimensions, View } from 'react-native';
|
|
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
8
|
import { COLOR_SURFACE_CONTAINER_LOW_DARK, COLOR_SURFACE_CONTAINER_LOW_LIGHT, DEFAULT_ANCHOR_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 { Drawer } from "./web/vaul/index.js";
|
|
10
10
|
import { DEFAULT_PEEK_HEIGHT, TRANSITIONS } from "./web/vaul/constants.js";
|
|
11
|
+
|
|
12
|
+
// First vertically scrollable descendant — web mirror of native's
|
|
13
|
+
// findScrollView (RN-web ScrollView/FlatList render a node with
|
|
14
|
+
// `overflow-y: auto`; its first child is the content container).
|
|
11
15
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
16
|
+
const findVerticalScroller = root => {
|
|
17
|
+
for (const el of root.querySelectorAll('*')) {
|
|
18
|
+
const {
|
|
19
|
+
overflowY
|
|
20
|
+
} = window.getComputedStyle(el);
|
|
21
|
+
if (overflowY === 'auto' || overflowY === 'scroll') return el;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
};
|
|
12
25
|
const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
13
26
|
const {
|
|
14
27
|
children,
|
|
@@ -31,9 +44,10 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
31
44
|
initialDetentIndex = -1,
|
|
32
45
|
header,
|
|
33
46
|
headerStyle,
|
|
47
|
+
headerOptions,
|
|
34
48
|
footer,
|
|
35
49
|
footerStyle,
|
|
36
|
-
|
|
50
|
+
footerOptions,
|
|
37
51
|
presentation = 'page',
|
|
38
52
|
detached = false,
|
|
39
53
|
detachedOffset = DEFAULT_DETACHED_OFFSET,
|
|
@@ -88,6 +102,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
88
102
|
}, [validDetents]);
|
|
89
103
|
const validDetentsRef = useRef(validDetents);
|
|
90
104
|
validDetentsRef.current = validDetents;
|
|
105
|
+
const hasAutoDetent = validDetents.includes('auto');
|
|
91
106
|
const handleSetActiveSnapPoint = useCallback(snapPoint => {
|
|
92
107
|
setActiveSnapPoint(snapPoint == null ? null : typeof snapPoint === 'number' || snapPoint === 'auto' || snapPoint === 'peek' ? snapPoint : null);
|
|
93
108
|
}, []);
|
|
@@ -174,7 +189,134 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
174
189
|
peekRef: peekElRef,
|
|
175
190
|
setPeekContentHeight
|
|
176
191
|
}), []);
|
|
177
|
-
|
|
192
|
+
|
|
193
|
+
// An absolute (floating) header overlaps the content, so it contributes no
|
|
194
|
+
// height to the 'auto' detent measurement.
|
|
195
|
+
const absoluteHeader = headerOptions?.position === 'absolute';
|
|
196
|
+
const absoluteHeaderRef = useRef(absoluteHeader);
|
|
197
|
+
absoluteHeaderRef.current = absoluteHeader;
|
|
198
|
+
const resolvedHeaderStyle = absoluteHeader ? [absoluteHeaderStyle, headerStyle] : headerStyle;
|
|
199
|
+
|
|
200
|
+
// Same for an absolute (floating) footer — rendered via vaul's
|
|
201
|
+
// `detachedSiblings` instead of in the content flow.
|
|
202
|
+
const absoluteFooter = footerOptions?.position === 'absolute';
|
|
203
|
+
const absoluteFooterRef = useRef(absoluteFooter);
|
|
204
|
+
absoluteFooterRef.current = absoluteFooter;
|
|
205
|
+
|
|
206
|
+
// The footer owns the sheet's bottom edge, so it absorbs the bottom
|
|
207
|
+
// safe-area inset as padding (on top of its own) — mirrors native, where the
|
|
208
|
+
// footer shadow node pads the inset so its background fills it.
|
|
209
|
+
const footerOwnsInset = Boolean(footer) && insetAdjustment === 'automatic';
|
|
210
|
+
const resolvedFooterStyle = useMemo(() => {
|
|
211
|
+
if (!footerOwnsInset) return footerStyle;
|
|
212
|
+
const flat = StyleSheet.flatten(footerStyle);
|
|
213
|
+
const base = flat?.paddingBottom ?? flat?.paddingVertical ?? flat?.padding;
|
|
214
|
+
const baseCss = typeof base === 'number' ? `${base}px` : typeof base === 'string' ? base : '0px';
|
|
215
|
+
return [footerStyle, {
|
|
216
|
+
paddingBottom: `calc(${baseCss} + env(safe-area-inset-bottom, 0px))`
|
|
217
|
+
}];
|
|
218
|
+
}, [footerOwnsInset, footerStyle]);
|
|
219
|
+
|
|
220
|
+
// A relative footer is laid out below the content, so it's pushed off-screen
|
|
221
|
+
// at the peek detent and contributes no height.
|
|
222
|
+
const peekHeight = (header ? headerHeight : 0) + (footer && absoluteFooter ? footerHeight : 0) + peekContentHeight || DEFAULT_PEEK_HEIGHT;
|
|
223
|
+
|
|
224
|
+
// Web mirror of native's scrollable handling for 'auto' detents: a plugged
|
|
225
|
+
// scrollable keeps the sized (bounded) layout so its viewport is capped to
|
|
226
|
+
// the visible sheet and can scroll, while the 'auto' height is measured with
|
|
227
|
+
// the viewport replaced by the scrollable's content size — mirrors native
|
|
228
|
+
// `naturalHeight`.
|
|
229
|
+
const [hasBoundedScrollable, setHasBoundedScrollable] = useState(false);
|
|
230
|
+
const [scrollableAutoHeight, setScrollableAutoHeight] = useState(0);
|
|
231
|
+
const pinnedScrollerRef = useRef(null);
|
|
232
|
+
|
|
233
|
+
// Natural content height (header + content + footer, with a pinned
|
|
234
|
+
// scrollable's viewport replaced by its content size) — the height the
|
|
235
|
+
// content wants regardless of the sheet's bounds.
|
|
236
|
+
const measureNaturalHeight = useCallback(() => {
|
|
237
|
+
const contentEl = contentRef.current;
|
|
238
|
+
if (!contentEl || !contentEl.isConnected) return 0;
|
|
239
|
+
const headerEl = absoluteHeaderRef.current ? null : headerElRef.current;
|
|
240
|
+
const footerEl = absoluteFooterRef.current ? null : footerElRef.current;
|
|
241
|
+
let height = (headerEl?.offsetHeight ?? 0) + (footerEl?.offsetHeight ?? 0) + contentEl.offsetHeight;
|
|
242
|
+
const scroller = pinnedScrollerRef.current;
|
|
243
|
+
const scrollContent = scroller?.firstElementChild;
|
|
244
|
+
if (scroller?.isConnected && scrollContent instanceof HTMLElement) {
|
|
245
|
+
height += scrollContent.offsetHeight - scroller.clientHeight;
|
|
246
|
+
}
|
|
247
|
+
return Math.max(0, height);
|
|
248
|
+
}, []);
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
if (!isOpen || !hasAutoDetent) return undefined;
|
|
251
|
+
let canceled = false;
|
|
252
|
+
let rafId = 0;
|
|
253
|
+
let mutationObserver = null;
|
|
254
|
+
let resizeObserver = null;
|
|
255
|
+
const attach = () => {
|
|
256
|
+
if (canceled) return;
|
|
257
|
+
const drawerEl = drawerContentRef.current;
|
|
258
|
+
if (!drawerEl || !drawerEl.isConnected) {
|
|
259
|
+
// Radix Presence defers the portal mount; poll until the drawer is live.
|
|
260
|
+
rafId = window.requestAnimationFrame(attach);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const measure = () => setScrollableAutoHeight(measureNaturalHeight());
|
|
264
|
+
let observed = null;
|
|
265
|
+
|
|
266
|
+
// (Re)pin the first vertical scrollable and observe the nodes whose size
|
|
267
|
+
// feeds the natural height — content growth inside a bounded scroller is
|
|
268
|
+
// invisible to the sheet's own layout (mirrors native's contentSize
|
|
269
|
+
// observation). Resolves the content node live: the layout-branch swap
|
|
270
|
+
// (natural flow ↔ sized) remounts it.
|
|
271
|
+
const observe = () => {
|
|
272
|
+
const contentEl = contentRef.current;
|
|
273
|
+
const headerEl = headerElRef.current;
|
|
274
|
+
|
|
275
|
+
// Skip mutations that don't change the observed topology — e.g. a
|
|
276
|
+
// virtualized list mounting rows inside the pinned scroller, which
|
|
277
|
+
// would otherwise trigger a full-subtree rescan (findVerticalScroller
|
|
278
|
+
// resolves computed styles) on every scroll frame. Size changes are
|
|
279
|
+
// already covered by the ResizeObserver.
|
|
280
|
+
if (observed && observed.contentEl === contentEl && contentEl?.isConnected && observed.headerEl === headerEl && observed.scroller.isConnected && observed.scrollContent === observed.scroller.firstElementChild) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const scroller = contentEl && contentEl.isConnected ? findVerticalScroller(contentEl) : null;
|
|
284
|
+
pinnedScrollerRef.current = scroller;
|
|
285
|
+
setHasBoundedScrollable(scroller != null);
|
|
286
|
+
observed = scroller ? {
|
|
287
|
+
contentEl,
|
|
288
|
+
headerEl,
|
|
289
|
+
scroller,
|
|
290
|
+
scrollContent: scroller.firstElementChild
|
|
291
|
+
} : null;
|
|
292
|
+
resizeObserver?.disconnect();
|
|
293
|
+
resizeObserver = new ResizeObserver(measure);
|
|
294
|
+
if (contentEl?.isConnected) resizeObserver.observe(contentEl);
|
|
295
|
+
if (headerEl) resizeObserver.observe(headerEl);
|
|
296
|
+
if (scroller) {
|
|
297
|
+
resizeObserver.observe(scroller);
|
|
298
|
+
if (scroller.firstElementChild) resizeObserver.observe(scroller.firstElementChild);
|
|
299
|
+
}
|
|
300
|
+
measure();
|
|
301
|
+
};
|
|
302
|
+
observe();
|
|
303
|
+
// Watch the whole drawer subtree — catches the scrollable mounting/
|
|
304
|
+
// unmounting AND the layout-branch swap, which remounts the content node
|
|
305
|
+
// (a content-scoped observer would go stale after the swap).
|
|
306
|
+
mutationObserver = new MutationObserver(observe);
|
|
307
|
+
mutationObserver.observe(drawerEl, {
|
|
308
|
+
childList: true,
|
|
309
|
+
subtree: true
|
|
310
|
+
});
|
|
311
|
+
};
|
|
312
|
+
rafId = window.requestAnimationFrame(attach);
|
|
313
|
+
return () => {
|
|
314
|
+
canceled = true;
|
|
315
|
+
window.cancelAnimationFrame(rafId);
|
|
316
|
+
mutationObserver?.disconnect();
|
|
317
|
+
resizeObserver?.disconnect();
|
|
318
|
+
};
|
|
319
|
+
}, [isOpen, hasAutoDetent, measureNaturalHeight]);
|
|
178
320
|
|
|
179
321
|
// Below the last detent a vertical touch pan moves the sheet, not the
|
|
180
322
|
// content — `[data-vaul-scroll-locked]` disables vertical touch panning on
|
|
@@ -266,13 +408,12 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
266
408
|
const effectiveH = inputs.effectiveDetached ? windowH - inputs.effectiveDetachedOffset : windowH;
|
|
267
409
|
// Matches vaul's height ceiling: min(effectiveH, maxContentHeight).
|
|
268
410
|
const ceiling = inputs.effectiveMaxContentHeight !== undefined ? Math.min(effectiveH, inputs.effectiveMaxContentHeight) : effectiveH;
|
|
269
|
-
// 'auto' resolves to the
|
|
411
|
+
// 'auto' resolves to the content's natural height. Read it live — the
|
|
270
412
|
// `measuredContentHeight` state lags mount by a frame (vaul's initial
|
|
271
413
|
// ref-callback measure runs while the portal subtree is still detached,
|
|
272
414
|
// so it reads 0 until the ResizeObserver fires). Falls back to the state
|
|
273
415
|
// value, then to vaul's pre-measure fallback (effectiveHeight / 2).
|
|
274
|
-
const
|
|
275
|
-
const measuredHeight = autoWrapper?.offsetHeight || inputs.measuredContentHeight;
|
|
416
|
+
const measuredHeight = measureNaturalHeight() || inputs.measuredContentHeight;
|
|
276
417
|
const autoHeight = Math.min(measuredHeight > 0 ? measuredHeight : effectiveH / 2, ceiling);
|
|
277
418
|
|
|
278
419
|
// 'peek' is measured live from the DOM (offsetHeight is layout-based, so
|
|
@@ -283,7 +424,8 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
283
424
|
// Geometry only runs while the drawer is mounted, so the elements are
|
|
284
425
|
// measurable here; fall back to the state value when they're absent.
|
|
285
426
|
const headerEl = headerElRef.current;
|
|
286
|
-
|
|
427
|
+
// A relative footer is pushed off-screen at the peek detent — excluded
|
|
428
|
+
const footerEl = absoluteFooterRef.current ? footerElRef.current : null;
|
|
287
429
|
const peekEl = peekElRef.current;
|
|
288
430
|
const contentEl = contentRef.current;
|
|
289
431
|
const livePeekContentHeight = peekEl && contentEl ? measurePeekContentHeight(peekEl, contentEl) : inputs.peekContentHeight;
|
|
@@ -297,10 +439,12 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
297
439
|
}
|
|
298
440
|
return {
|
|
299
441
|
windowH,
|
|
442
|
+
effectiveH,
|
|
443
|
+
ceiling,
|
|
300
444
|
positions,
|
|
301
445
|
values
|
|
302
446
|
};
|
|
303
|
-
}, []);
|
|
447
|
+
}, [measureNaturalHeight]);
|
|
304
448
|
|
|
305
449
|
// Detent info for lifecycle events. Position/detent come from the active
|
|
306
450
|
// detent's target geometry — not the live DOM rect — so willPresent (drawer
|
|
@@ -387,7 +531,46 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
387
531
|
detent: values[count - 1]
|
|
388
532
|
};
|
|
389
533
|
}, [computeDetentGeometry]);
|
|
534
|
+
|
|
535
|
+
// Mirror native synchronous layout: while dragging (and through the post-
|
|
536
|
+
// release settle) the sized layout tracks the sheet's visible height in
|
|
537
|
+
// realtime, clamped to the smallest detent — dragging below it slides the
|
|
538
|
+
// sheet out without resizing. Once settled, the inline height is cleared so
|
|
539
|
+
// the CSS calc() owns sizing again (adapts to viewport resizes at rest).
|
|
540
|
+
const sizedLayoutRef = useRef(null);
|
|
541
|
+
const isDraggingRef = useRef(false);
|
|
542
|
+
const isSettlingAfterDragRef = useRef(false);
|
|
543
|
+
const updateSizedLayoutHeight = useCallback(position => {
|
|
544
|
+
const node = sizedLayoutRef.current;
|
|
545
|
+
if (!node) return;
|
|
546
|
+
const {
|
|
547
|
+
effectiveH,
|
|
548
|
+
ceiling,
|
|
549
|
+
positions
|
|
550
|
+
} = computeDetentGeometry();
|
|
551
|
+
if (positions.length === 0) return;
|
|
552
|
+
if (isSettlingAfterDragRef.current) {
|
|
553
|
+
const snap = activeSnapPointRef.current;
|
|
554
|
+
const index = snap != null ? validDetentsRef.current.indexOf(snap) : -1;
|
|
555
|
+
const target = index >= 0 ? positions[index] : null;
|
|
556
|
+
if (target != null && Math.abs(position - target) < 1) {
|
|
557
|
+
isSettlingAfterDragRef.current = false;
|
|
558
|
+
// Restore the calc — height is an inline style, so clearing it would
|
|
559
|
+
// leave the div unsized (React won't re-apply it without a render)
|
|
560
|
+
node.style.height = SIZED_LAYOUT_HEIGHT;
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// positions[0] is the smallest detent's top-Y — its height is the clamp floor.
|
|
566
|
+
const minHeight = effectiveH - positions[0];
|
|
567
|
+
const height = Math.min(Math.max(effectiveH - position, minHeight), ceiling);
|
|
568
|
+
node.style.height = `${height}px`;
|
|
569
|
+
}, [computeDetentGeometry]);
|
|
390
570
|
const handlePositionChange = useCallback(position => {
|
|
571
|
+
if (isDraggingRef.current || isSettlingAfterDragRef.current) {
|
|
572
|
+
updateSizedLayoutHeight(position);
|
|
573
|
+
}
|
|
391
574
|
const {
|
|
392
575
|
index,
|
|
393
576
|
detent
|
|
@@ -400,7 +583,7 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
400
583
|
realtime: true
|
|
401
584
|
}
|
|
402
585
|
});
|
|
403
|
-
}, [interpolateFromPosition]);
|
|
586
|
+
}, [interpolateFromPosition, updateSizedLayoutHeight]);
|
|
404
587
|
|
|
405
588
|
// Fire onMount once after first render. React-mount is the earliest point
|
|
406
589
|
// the component is ready for imperative calls, matching the native
|
|
@@ -532,7 +715,6 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
532
715
|
|
|
533
716
|
// Vaul's `onDrag` fires once per pointermove while dragging; the first tick
|
|
534
717
|
// after an idle gap marks the drag boundary, so track it via a ref.
|
|
535
|
-
const isDraggingRef = useRef(false);
|
|
536
718
|
const handleDrag = useCallback(() => {
|
|
537
719
|
if (!isDraggingRef.current) {
|
|
538
720
|
isDraggingRef.current = true;
|
|
@@ -544,9 +726,12 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
544
726
|
nativeEvent: computeDetentInfo(true)
|
|
545
727
|
});
|
|
546
728
|
}, [computeDetentInfo]);
|
|
547
|
-
const handleRelease = useCallback(() => {
|
|
729
|
+
const handleRelease = useCallback((_event, open) => {
|
|
548
730
|
if (!isDraggingRef.current) return;
|
|
549
731
|
isDraggingRef.current = false;
|
|
732
|
+
// Track the sized layout through the settle animation only when the sheet
|
|
733
|
+
// stays open — a dismissal slides out at the min-clamped height.
|
|
734
|
+
isSettlingAfterDragRef.current = open;
|
|
550
735
|
onDragEndRef.current?.({
|
|
551
736
|
nativeEvent: computeDetentInfo(true)
|
|
552
737
|
});
|
|
@@ -737,6 +922,11 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
737
922
|
window.cancelAnimationFrame(rafId);
|
|
738
923
|
};
|
|
739
924
|
}, [isOpen, descendants.length]);
|
|
925
|
+
|
|
926
|
+
// Definite-height flex layout (per-detent sizing) unless content must be
|
|
927
|
+
// measured in natural flow: 'auto' detents (without a plugged scrollable —
|
|
928
|
+
// those measure via `measureNaturalHeight`) and form-sheet content-fit sizing.
|
|
929
|
+
const useSizedLayout = (!hasAutoDetent || hasBoundedScrollable) && !isFormSheet;
|
|
740
930
|
const effectiveCornerRadius = cornerRadius ?? DEFAULT_CORNER_RADIUS;
|
|
741
931
|
|
|
742
932
|
// Shadow cast upward from the sheet's top edge toward the background. Matches
|
|
@@ -762,11 +952,16 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
762
952
|
borderTopRightRadius: effectiveCornerRadius,
|
|
763
953
|
backgroundColor: backgroundColor,
|
|
764
954
|
// Clip children to the rounded top so headers/content with their own
|
|
765
|
-
// background don't bleed past the corners.
|
|
766
|
-
|
|
955
|
+
// background don't bleed past the corners. `clip` (not `hidden`): a
|
|
956
|
+
// hidden box is still a scroll container, so browser focus-reveal (e.g.
|
|
957
|
+
// tapping a button near the sheet bottom) can scroll-offset the drawer —
|
|
958
|
+
// shifting content and blocking vaul's shouldDrag walk (scrollTop > 0).
|
|
959
|
+
overflow: 'clip',
|
|
767
960
|
// Lift content above iOS home indicator / bottom safe area when enabled.
|
|
768
|
-
|
|
769
|
-
|
|
961
|
+
// A relative footer owns the inset instead (see resolvedFooterStyle); an
|
|
962
|
+
// absolute footer floats over the content, so the content keeps its lift.
|
|
963
|
+
paddingBottom: insetAdjustment === 'automatic' && !(footerOwnsInset && !absoluteFooter) ? 'env(safe-area-inset-bottom, 0px)' : 0
|
|
964
|
+
}), [backgroundColor, effectiveCornerRadius, insetAdjustment, footerOwnsInset, absoluteFooter]);
|
|
770
965
|
const defaultGrabberColor = colorScheme === 'dark' ? DEFAULT_GRABBER_COLOR_DARK : DEFAULT_GRABBER_COLOR_LIGHT;
|
|
771
966
|
const grabberHeight = grabberOptions?.height ?? DEFAULT_GRABBER_HEIGHT;
|
|
772
967
|
|
|
@@ -867,6 +1062,10 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
867
1062
|
initialAnimated: initialDetentAnimated,
|
|
868
1063
|
detachedWrapperStyle: wrapperStyle,
|
|
869
1064
|
onContentHeightChange: setMeasuredContentHeight,
|
|
1065
|
+
contentHeight:
|
|
1066
|
+
// Bounded scrollable → the auto-size wrapper measures 0 (sized
|
|
1067
|
+
// layout is absolute), so feed vaul the natural height instead.
|
|
1068
|
+
hasAutoDetent && hasBoundedScrollable && scrollableAutoHeight > 0 ? scrollableAutoHeight : undefined,
|
|
870
1069
|
activeSnapPoint: activeSnapPoint,
|
|
871
1070
|
setActiveSnapPoint: handleSetActiveSnapPoint,
|
|
872
1071
|
...snapPointsProps,
|
|
@@ -878,11 +1077,11 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
878
1077
|
ref: drawerContentRef,
|
|
879
1078
|
style: mergedContentStyle,
|
|
880
1079
|
onPointerDownOutside: handlePointerDownOutside,
|
|
881
|
-
detachedSiblings: footer ? /*#__PURE__*/_jsx("div", {
|
|
1080
|
+
detachedSiblings: footer && absoluteFooter ? /*#__PURE__*/_jsx("div", {
|
|
882
1081
|
style: footerFloatStyle,
|
|
883
1082
|
children: /*#__PURE__*/_jsx(View, {
|
|
884
1083
|
ref: footerElRef,
|
|
885
|
-
style:
|
|
1084
|
+
style: resolvedFooterStyle,
|
|
886
1085
|
onLayout: handleFooterLayout,
|
|
887
1086
|
children: /*#__PURE__*/isValidElement(footer) ? footer : /*#__PURE__*/createElement(footer)
|
|
888
1087
|
})
|
|
@@ -892,39 +1091,52 @@ const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
892
1091
|
children: accessibilityOptions?.paneTitle ?? 'Sheet'
|
|
893
1092
|
}), grabber && /*#__PURE__*/_jsx(Drawer.Handle, {
|
|
894
1093
|
style: handleStyle
|
|
895
|
-
}),
|
|
1094
|
+
}), useSizedLayout ?
|
|
896
1095
|
/*#__PURE__*/
|
|
897
1096
|
// vaul wraps children in `[data-vaul-auto-size-wrapper]` (display:
|
|
898
1097
|
// flow-root) which doesn't honor descendant flex layout. Use an
|
|
899
1098
|
// absolute fill sized to the visible portion (via vaul's
|
|
900
1099
|
// `--snap-point-height` var) so the inner flex column has a
|
|
901
|
-
// definite height for
|
|
1100
|
+
// definite height for flex layouts to fill — mirrors native, where
|
|
1101
|
+
// the container is sized to the sheet's visible height per detent.
|
|
902
1102
|
_jsxs("div", {
|
|
903
|
-
|
|
1103
|
+
ref: sizedLayoutRef,
|
|
1104
|
+
style: sizedLayoutStyle,
|
|
1105
|
+
"data-vaul-scroll-locked": isScrollLocked ? '' : undefined,
|
|
904
1106
|
children: [header && /*#__PURE__*/_jsx(View, {
|
|
905
1107
|
ref: headerElRef,
|
|
906
|
-
style:
|
|
1108
|
+
style: resolvedHeaderStyle,
|
|
907
1109
|
onLayout: handleHeaderLayout,
|
|
908
1110
|
children: /*#__PURE__*/isValidElement(header) ? header : /*#__PURE__*/createElement(header)
|
|
909
|
-
}), /*#__PURE__*/_jsx(
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
children:
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1111
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
1112
|
+
ref: contentRef,
|
|
1113
|
+
style: hasAutoDetent && hasBoundedScrollable ? [contentFillStyle, style] : style,
|
|
1114
|
+
children: children
|
|
1115
|
+
}), footer && !absoluteFooter && /*#__PURE__*/_jsx(View, {
|
|
1116
|
+
ref: footerElRef,
|
|
1117
|
+
style: [relativeFooterStyle, resolvedFooterStyle],
|
|
1118
|
+
onLayout: handleFooterLayout,
|
|
1119
|
+
children: /*#__PURE__*/isValidElement(footer) ? footer : /*#__PURE__*/createElement(footer)
|
|
917
1120
|
})]
|
|
918
|
-
}) :
|
|
1121
|
+
}) :
|
|
1122
|
+
/*#__PURE__*/
|
|
1123
|
+
// Natural flow so vaul can measure content height — required for
|
|
1124
|
+
// 'auto' detents and form-sheet content-fit sizing.
|
|
1125
|
+
_jsxs(_Fragment, {
|
|
919
1126
|
children: [header && /*#__PURE__*/_jsx(View, {
|
|
920
1127
|
ref: headerElRef,
|
|
921
|
-
style:
|
|
1128
|
+
style: resolvedHeaderStyle,
|
|
922
1129
|
onLayout: handleHeaderLayout,
|
|
923
1130
|
children: /*#__PURE__*/isValidElement(header) ? header : /*#__PURE__*/createElement(header)
|
|
924
1131
|
}), /*#__PURE__*/_jsx(View, {
|
|
925
1132
|
ref: contentRef,
|
|
926
1133
|
style: style,
|
|
927
1134
|
children: children
|
|
1135
|
+
}), footer && !absoluteFooter && /*#__PURE__*/_jsx(View, {
|
|
1136
|
+
ref: footerElRef,
|
|
1137
|
+
style: resolvedFooterStyle,
|
|
1138
|
+
onLayout: handleFooterLayout,
|
|
1139
|
+
children: /*#__PURE__*/isValidElement(footer) ? footer : /*#__PURE__*/createElement(footer)
|
|
928
1140
|
})]
|
|
929
1141
|
})]
|
|
930
1142
|
})]
|
|
@@ -937,21 +1149,33 @@ const overlayStyle = {
|
|
|
937
1149
|
inset: 0,
|
|
938
1150
|
backgroundColor: 'rgba(0, 0, 0, 0.5)'
|
|
939
1151
|
};
|
|
940
|
-
|
|
1152
|
+
|
|
1153
|
+
// Pinned to the sheet's bottom edge — takes up layout space below the content
|
|
1154
|
+
const relativeFooterStyle = {
|
|
1155
|
+
marginTop: 'auto'
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
// Floats over the content so it doesn't take up layout space
|
|
1159
|
+
const absoluteHeaderStyle = {
|
|
1160
|
+
position: 'absolute',
|
|
1161
|
+
top: 0,
|
|
1162
|
+
left: 0,
|
|
1163
|
+
right: 0,
|
|
1164
|
+
zIndex: 1
|
|
1165
|
+
};
|
|
1166
|
+
const SIZED_LAYOUT_HEIGHT = 'calc(100% - var(--snap-point-height, 0px))';
|
|
1167
|
+
const sizedLayoutStyle = {
|
|
941
1168
|
position: 'absolute',
|
|
942
1169
|
top: 0,
|
|
943
1170
|
left: 0,
|
|
944
1171
|
right: 0,
|
|
945
|
-
height:
|
|
1172
|
+
height: SIZED_LAYOUT_HEIGHT,
|
|
946
1173
|
display: 'flex',
|
|
947
1174
|
flexDirection: 'column'
|
|
948
1175
|
};
|
|
949
|
-
const
|
|
1176
|
+
const contentFillStyle = {
|
|
950
1177
|
flex: 1,
|
|
951
|
-
minHeight: 0
|
|
952
|
-
overflowY: 'auto',
|
|
953
|
-
overscrollBehavior: 'contain',
|
|
954
|
-
touchAction: 'pan-y'
|
|
1178
|
+
minHeight: 0
|
|
955
1179
|
};
|
|
956
1180
|
const visuallyHiddenStyle = {
|
|
957
1181
|
position: 'absolute',
|