@lodev09/react-native-true-sheet 3.11.5 → 3.11.7
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/core/TrueSheetCoordinatorLayout.kt +46 -19
- package/ios/TrueSheetViewController.mm +27 -15
- package/lib/module/TrueSheet.web.js +108 -66
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/web/vaul/index.js +76 -9
- package/lib/module/web/vaul/index.js.map +1 -1
- package/lib/module/web/vaul/style.css +24 -1
- package/lib/module/web/vaul/use-snap-points.js +13 -4
- package/lib/module/web/vaul/use-snap-points.js.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/TrueSheet.web.tsx +124 -78
- package/src/web/vaul/index.tsx +76 -9
- package/src/web/vaul/style.css +24 -1
- package/src/web/vaul/use-snap-points.ts +14 -5
package/src/TrueSheet.web.tsx
CHANGED
|
@@ -255,6 +255,42 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
255
255
|
(header ? headerHeight : 0) + (footer ? footerHeight : 0) + peekContentHeight ||
|
|
256
256
|
DEFAULT_PEEK_HEIGHT;
|
|
257
257
|
|
|
258
|
+
// Below the last detent a vertical touch pan moves the sheet, not the
|
|
259
|
+
// content — `[data-vaul-scroll-locked]` disables vertical touch panning on
|
|
260
|
+
// the scroll container and everything inside it (see vaul/style.css).
|
|
261
|
+
const isScrollLocked =
|
|
262
|
+
validDetents.length > 0 && activeSnapPoint !== validDetents[validDetents.length - 1];
|
|
263
|
+
|
|
264
|
+
// Vaul measures the auto-size wrapper's offsetHeight (always, post fork).
|
|
265
|
+
// Track it here so the form sheet can size its card to fit content,
|
|
266
|
+
// clamped between a minimum ratio of the viewport and a maximum derived
|
|
267
|
+
// from `detachedOffset` (the breathing room left at top + bottom of the
|
|
268
|
+
// floating card).
|
|
269
|
+
const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
|
|
270
|
+
|
|
271
|
+
const effectiveMaxContentHeight = useMemo<number | undefined>(() => {
|
|
272
|
+
if (maxContentHeight !== undefined) return maxContentHeight;
|
|
273
|
+
if (!isFormSheet) return undefined;
|
|
274
|
+
const min = windowHeight * DEFAULT_FORM_SHEET_HEIGHT_RATIO;
|
|
275
|
+
const max = Math.max(min, windowHeight - 2 * detachedOffset);
|
|
276
|
+
if (measuredContentHeight <= 0) return min;
|
|
277
|
+
return Math.max(min, Math.min(measuredContentHeight, max));
|
|
278
|
+
}, [maxContentHeight, isFormSheet, windowHeight, detachedOffset, measuredContentHeight]);
|
|
279
|
+
|
|
280
|
+
// Center the form sheet using the actual visible drawer height. Vaul
|
|
281
|
+
// auto-sizes to content (capped by `maxContentHeight`), so when content is
|
|
282
|
+
// shorter than `effectiveMaxContentHeight`'s min-clamped floor, using that
|
|
283
|
+
// for the offset would push a small sheet below the viewport center.
|
|
284
|
+
const effectiveDetachedOffset = useMemo(() => {
|
|
285
|
+
if (!isFormSheet) return detachedOffset;
|
|
286
|
+
const max = Math.max(0, windowHeight - 2 * detachedOffset);
|
|
287
|
+
const visibleHeight =
|
|
288
|
+
measuredContentHeight > 0
|
|
289
|
+
? Math.min(measuredContentHeight, max)
|
|
290
|
+
: (effectiveMaxContentHeight ?? 0);
|
|
291
|
+
return Math.max(0, (windowHeight - visibleHeight) / 2);
|
|
292
|
+
}, [isFormSheet, windowHeight, detachedOffset, measuredContentHeight, effectiveMaxContentHeight]);
|
|
293
|
+
|
|
258
294
|
// Present/dismiss events. The sheet settles via a CSS `transform` transition
|
|
259
295
|
// on either the drawer (snap-points on autopresent) or the wrapper (whole-
|
|
260
296
|
// card slide on reopen/dismiss). `Animation.finished` from the Web Animations
|
|
@@ -285,54 +321,91 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
285
321
|
activeSnapPointRef.current = activeSnapPoint;
|
|
286
322
|
});
|
|
287
323
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
324
|
+
// Detent geometry — target top-Y (`positions`) and height ratio (`values`)
|
|
325
|
+
// per detent. Mirrors vaul's snap-offset math exactly (same effective
|
|
326
|
+
// height, ceiling, and 'auto'/'peek' resolution) so computed targets match
|
|
327
|
+
// where the drawer actually settles. Numeric detent d → top-Y =
|
|
328
|
+
// (1 - d) * effectiveH. 'auto' resolves to the auto-size wrapper's measured
|
|
329
|
+
// height (tracked via vaul's `onContentHeightChange`) — same signal vaul
|
|
330
|
+
// uses to compute its snap offset. Inputs live in a render-synced ref so
|
|
331
|
+
// the compute callbacks stay referentially stable for the event effects.
|
|
332
|
+
const geometryInputsRef = useRef({
|
|
333
|
+
effectiveDetached,
|
|
334
|
+
effectiveDetachedOffset,
|
|
335
|
+
effectiveMaxContentHeight,
|
|
336
|
+
peekHeight,
|
|
337
|
+
measuredContentHeight,
|
|
338
|
+
});
|
|
339
|
+
geometryInputsRef.current = {
|
|
340
|
+
effectiveDetached,
|
|
341
|
+
effectiveDetachedOffset,
|
|
342
|
+
effectiveMaxContentHeight,
|
|
343
|
+
peekHeight,
|
|
344
|
+
measuredContentHeight,
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const computeDetentGeometry = useCallback(() => {
|
|
348
|
+
const inputs = geometryInputsRef.current;
|
|
349
|
+
const windowH = window.innerHeight;
|
|
350
|
+
const effectiveH = inputs.effectiveDetached
|
|
351
|
+
? windowH - inputs.effectiveDetachedOffset
|
|
352
|
+
: windowH;
|
|
353
|
+
// Matches vaul's height ceiling: min(effectiveH, maxContentHeight).
|
|
354
|
+
const ceiling =
|
|
355
|
+
inputs.effectiveMaxContentHeight !== undefined
|
|
356
|
+
? Math.min(effectiveH, inputs.effectiveMaxContentHeight)
|
|
357
|
+
: effectiveH;
|
|
358
|
+
// Matches vaul's 'auto' fallback (effectiveHeight / 2) before content is
|
|
359
|
+
// measured — keeps targets consistent even before the drawer mounts.
|
|
360
|
+
const autoHeight = Math.min(
|
|
361
|
+
inputs.measuredContentHeight > 0 ? inputs.measuredContentHeight : effectiveH / 2,
|
|
362
|
+
ceiling
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
const positions: number[] = [];
|
|
366
|
+
const values: number[] = [];
|
|
367
|
+
for (const d of validDetentsRef.current) {
|
|
368
|
+
const h =
|
|
369
|
+
typeof d === 'number'
|
|
370
|
+
? Math.min(d * effectiveH, ceiling)
|
|
371
|
+
: d === 'peek'
|
|
372
|
+
? Math.min(inputs.peekHeight, ceiling)
|
|
373
|
+
: autoHeight;
|
|
374
|
+
positions.push(effectiveH - h);
|
|
375
|
+
values.push(effectiveH > 0 ? h / effectiveH : 0);
|
|
376
|
+
}
|
|
377
|
+
return { windowH, positions, values };
|
|
294
378
|
}, []);
|
|
295
379
|
|
|
380
|
+
// Detent info for lifecycle events. Position/detent come from the active
|
|
381
|
+
// detent's target geometry — not the live DOM rect — so willPresent (drawer
|
|
382
|
+
// not mounted yet), detentChange (animation just started), and didPresent
|
|
383
|
+
// all emit the settled detent position, matching iOS/Android. Drag events
|
|
384
|
+
// pass `live: true` to report the in-flight rect position instead, also
|
|
385
|
+
// matching native.
|
|
386
|
+
const computeDetentInfo = useCallback(
|
|
387
|
+
(live = false): DetentInfoEventPayload => {
|
|
388
|
+
const snap = activeSnapPointRef.current;
|
|
389
|
+
const index = snap != null ? validDetentsRef.current.indexOf(snap) : -1;
|
|
390
|
+
const { windowH, positions, values } = computeDetentGeometry();
|
|
391
|
+
const target = index >= 0 ? positions[index] : undefined;
|
|
392
|
+
const position = live
|
|
393
|
+
? (drawerContentRef.current?.getBoundingClientRect().top ?? target ?? windowH)
|
|
394
|
+
: (target ?? windowH);
|
|
395
|
+
return { index, position, detent: index >= 0 ? (values[index] ?? 0) : 0 };
|
|
396
|
+
},
|
|
397
|
+
[computeDetentGeometry]
|
|
398
|
+
);
|
|
399
|
+
|
|
296
400
|
// Mirror Android: interpolate fractional index and detent from the drawer's
|
|
297
401
|
// top-Y so continuous position updates (drag, animation) carry smooth values
|
|
298
|
-
// between detent boundaries.
|
|
299
|
-
// 'auto' resolves to the [data-vaul-auto-size-wrapper] element's measured
|
|
300
|
-
// offsetHeight — same signal vaul uses to compute its snap offset.
|
|
402
|
+
// between detent boundaries.
|
|
301
403
|
const interpolateFromPosition = useCallback(
|
|
302
404
|
(position: number): { index: number; detent: number } => {
|
|
303
|
-
const
|
|
304
|
-
const count =
|
|
405
|
+
const { windowH, positions, values } = computeDetentGeometry();
|
|
406
|
+
const count = positions.length;
|
|
305
407
|
if (count === 0) return { index: -1, detent: 0 };
|
|
306
408
|
|
|
307
|
-
const windowH = window.innerHeight;
|
|
308
|
-
const effectiveH = effectiveDetached ? windowH - detachedOffset : windowH;
|
|
309
|
-
// Matches vaul's height ceiling: min(effectiveH, maxContentHeight).
|
|
310
|
-
const ceiling =
|
|
311
|
-
maxContentHeight !== undefined ? Math.min(effectiveH, maxContentHeight) : effectiveH;
|
|
312
|
-
|
|
313
|
-
const autoWrapper = drawerContentRef.current?.querySelector<HTMLElement>(
|
|
314
|
-
'[data-vaul-auto-size-wrapper]'
|
|
315
|
-
);
|
|
316
|
-
const autoHeight = Math.min(autoWrapper?.offsetHeight ?? ceiling / 2, ceiling);
|
|
317
|
-
|
|
318
|
-
const positions: number[] = [];
|
|
319
|
-
const values: number[] = [];
|
|
320
|
-
for (let i = 0; i < count; i++) {
|
|
321
|
-
const d = snaps[i];
|
|
322
|
-
if (typeof d === 'number') {
|
|
323
|
-
const h = Math.min(d * effectiveH, ceiling);
|
|
324
|
-
positions.push(effectiveH - h);
|
|
325
|
-
values.push(effectiveH > 0 ? h / effectiveH : 0);
|
|
326
|
-
} else if (d === 'peek') {
|
|
327
|
-
const h = Math.min(peekHeight, ceiling);
|
|
328
|
-
positions.push(effectiveH - h);
|
|
329
|
-
values.push(effectiveH > 0 ? h / effectiveH : 0);
|
|
330
|
-
} else {
|
|
331
|
-
positions.push(effectiveH - autoHeight);
|
|
332
|
-
values.push(effectiveH > 0 ? autoHeight / effectiveH : 0);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
409
|
// Absorb subpixel drift from getBoundingClientRect so at-rest positions
|
|
337
410
|
// don't sneak into the below-first branch and emit near-zero negatives
|
|
338
411
|
// like `-1e-8` (which render as "-1" via JS scientific-notation toString).
|
|
@@ -378,7 +451,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
378
451
|
|
|
379
452
|
return { index: count - 1, detent: values[count - 1]! };
|
|
380
453
|
},
|
|
381
|
-
[
|
|
454
|
+
[computeDetentGeometry]
|
|
382
455
|
);
|
|
383
456
|
|
|
384
457
|
const handlePositionChange = useCallback(
|
|
@@ -504,14 +577,14 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
504
577
|
const handleDrag = useCallback(() => {
|
|
505
578
|
if (!isDraggingRef.current) {
|
|
506
579
|
isDraggingRef.current = true;
|
|
507
|
-
onDragBeginRef.current?.({ nativeEvent: computeDetentInfo() } as DragBeginEvent);
|
|
580
|
+
onDragBeginRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragBeginEvent);
|
|
508
581
|
}
|
|
509
|
-
onDragChangeRef.current?.({ nativeEvent: computeDetentInfo() } as DragChangeEvent);
|
|
582
|
+
onDragChangeRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragChangeEvent);
|
|
510
583
|
}, [computeDetentInfo]);
|
|
511
584
|
const handleRelease = useCallback(() => {
|
|
512
585
|
if (!isDraggingRef.current) return;
|
|
513
586
|
isDraggingRef.current = false;
|
|
514
|
-
onDragEndRef.current?.({ nativeEvent: computeDetentInfo() } as DragEndEvent);
|
|
587
|
+
onDragEndRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragEndEvent);
|
|
515
588
|
}, [computeDetentInfo]);
|
|
516
589
|
|
|
517
590
|
const { isNested, dismissAbove, descendants } = useSheetStack(
|
|
@@ -764,39 +837,9 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
764
837
|
// Form-sheet style (presentation='form'): centered floating card with a
|
|
765
838
|
// default width and a height fit to content. We reuse the existing detached
|
|
766
839
|
// mechanic so drag/snap math stays correct — the wrapper is bottom-attached
|
|
767
|
-
// with a computed offset
|
|
768
|
-
//
|
|
769
|
-
// DEFAULT_FORM_SHEET_WIDTH.
|
|
770
|
-
|
|
771
|
-
// Vaul measures the auto-size wrapper's offsetHeight (always, post fork).
|
|
772
|
-
// Track it here so the form sheet can size its card to fit content,
|
|
773
|
-
// clamped between a minimum ratio of the viewport and a maximum derived
|
|
774
|
-
// from `detachedOffset` (the breathing room left at top + bottom of the
|
|
775
|
-
// floating card).
|
|
776
|
-
const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
|
|
777
|
-
|
|
778
|
-
const effectiveMaxContentHeight = useMemo<number | undefined>(() => {
|
|
779
|
-
if (maxContentHeight !== undefined) return maxContentHeight;
|
|
780
|
-
if (!isFormSheet) return undefined;
|
|
781
|
-
const min = windowHeight * DEFAULT_FORM_SHEET_HEIGHT_RATIO;
|
|
782
|
-
const max = Math.max(min, windowHeight - 2 * detachedOffset);
|
|
783
|
-
if (measuredContentHeight <= 0) return min;
|
|
784
|
-
return Math.max(min, Math.min(measuredContentHeight, max));
|
|
785
|
-
}, [maxContentHeight, isFormSheet, windowHeight, detachedOffset, measuredContentHeight]);
|
|
786
|
-
|
|
787
|
-
// Center the form sheet using the actual visible drawer height. Vaul
|
|
788
|
-
// auto-sizes to content (capped by `maxContentHeight`), so when content is
|
|
789
|
-
// shorter than `effectiveMaxContentHeight`'s min-clamped floor, using that
|
|
790
|
-
// for the offset would push a small sheet below the viewport center.
|
|
791
|
-
const effectiveDetachedOffset = useMemo(() => {
|
|
792
|
-
if (!isFormSheet) return detachedOffset;
|
|
793
|
-
const max = Math.max(0, windowHeight - 2 * detachedOffset);
|
|
794
|
-
const visibleHeight =
|
|
795
|
-
measuredContentHeight > 0
|
|
796
|
-
? Math.min(measuredContentHeight, max)
|
|
797
|
-
: (effectiveMaxContentHeight ?? 0);
|
|
798
|
-
return Math.max(0, (windowHeight - visibleHeight) / 2);
|
|
799
|
-
}, [isFormSheet, windowHeight, detachedOffset, measuredContentHeight, effectiveMaxContentHeight]);
|
|
840
|
+
// with a computed offset (`effectiveDetachedOffset`, declared above) that
|
|
841
|
+
// centers it vertically. `presentation` is absolute: when 'form',
|
|
842
|
+
// `maxContentWidth` is ignored and the card uses DEFAULT_FORM_SHEET_WIDTH.
|
|
800
843
|
|
|
801
844
|
// The wrapper holds all horizontal sizing/anchoring so its rounded-bottom
|
|
802
845
|
// clip (when detached) aligns with the drawer's horizontal bounds on
|
|
@@ -930,7 +973,10 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
|
|
|
930
973
|
{isValidElement(header) ? header : createElement(header)}
|
|
931
974
|
</View>
|
|
932
975
|
)}
|
|
933
|
-
<div
|
|
976
|
+
<div
|
|
977
|
+
style={scrollableContainerStyle}
|
|
978
|
+
data-vaul-scroll-locked={isScrollLocked ? '' : undefined}
|
|
979
|
+
>
|
|
934
980
|
<View ref={contentRef} style={style}>
|
|
935
981
|
{children}
|
|
936
982
|
</View>
|
package/src/web/vaul/index.tsx
CHANGED
|
@@ -24,7 +24,7 @@ import type { DrawerDirection } from './types';
|
|
|
24
24
|
import { useComposedRefs } from './use-composed-refs';
|
|
25
25
|
import { useControllableState } from './use-controllable-state';
|
|
26
26
|
import { usePositionFixed } from './use-position-fixed';
|
|
27
|
-
import { isInput, usePreventScroll } from './use-prevent-scroll';
|
|
27
|
+
import { isInput, isScrollable, usePreventScroll } from './use-prevent-scroll';
|
|
28
28
|
import { useScaleBackground } from './use-scale-background';
|
|
29
29
|
import { useSnapPoints } from './use-snap-points';
|
|
30
30
|
|
|
@@ -337,6 +337,44 @@ export function Root({
|
|
|
337
337
|
noBodyStyles,
|
|
338
338
|
});
|
|
339
339
|
|
|
340
|
+
// While the sheet itself is being dragged, scrollables in the touched chain
|
|
341
|
+
// are frozen (overflow: hidden). Touch browsers latch the scroll gesture at
|
|
342
|
+
// touchstart and ignore preventDefault once scrolling has started, so making
|
|
343
|
+
// the scroller non-scrollable mid-gesture is the only reliable way to keep
|
|
344
|
+
// the content from panning along with the sheet.
|
|
345
|
+
const frozenScrollablesRef = React.useRef<
|
|
346
|
+
{ element: HTMLElement; overflowX: string; overflowY: string }[] | null
|
|
347
|
+
>(null);
|
|
348
|
+
|
|
349
|
+
function freezeScrollables(target: EventTarget) {
|
|
350
|
+
if (frozenScrollablesRef.current) return;
|
|
351
|
+
const frozen: { element: HTMLElement; overflowX: string; overflowY: string }[] = [];
|
|
352
|
+
let element = target instanceof HTMLElement ? target : null;
|
|
353
|
+
while (element && element !== drawerRef.current) {
|
|
354
|
+
if (isScrollable(element)) {
|
|
355
|
+
frozen.push({
|
|
356
|
+
element,
|
|
357
|
+
overflowX: element.style.overflowX,
|
|
358
|
+
overflowY: element.style.overflowY,
|
|
359
|
+
});
|
|
360
|
+
element.style.overflowX = 'hidden';
|
|
361
|
+
element.style.overflowY = 'hidden';
|
|
362
|
+
}
|
|
363
|
+
element = element.parentElement;
|
|
364
|
+
}
|
|
365
|
+
frozenScrollablesRef.current = frozen;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function unfreezeScrollables() {
|
|
369
|
+
const frozen = frozenScrollablesRef.current;
|
|
370
|
+
if (!frozen) return;
|
|
371
|
+
frozenScrollablesRef.current = null;
|
|
372
|
+
for (const { element, overflowX, overflowY } of frozen) {
|
|
373
|
+
element.style.overflowX = overflowX;
|
|
374
|
+
element.style.overflowY = overflowY;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
340
378
|
function getScale() {
|
|
341
379
|
return (window.innerWidth - WINDOW_TOP_OFFSET) / window.innerWidth;
|
|
342
380
|
}
|
|
@@ -386,7 +424,16 @@ export function Root({
|
|
|
386
424
|
}
|
|
387
425
|
|
|
388
426
|
if (swipeAmount !== null) {
|
|
389
|
-
|
|
427
|
+
// Translated past the drag threshold → keep dragging. Below the last
|
|
428
|
+
// snap point the threshold is 0, so the sheet always wins over content
|
|
429
|
+
// scrolling. At the last snap point the threshold is its resting
|
|
430
|
+
// translate — which can still be > 0 when the max detent < 1 — so an
|
|
431
|
+
// at-rest sheet falls through to the scroll checks and content
|
|
432
|
+
// scrolling wins, matching native. Mid-drag/mid-animation the sheet is
|
|
433
|
+
// displaced past rest and still wins.
|
|
434
|
+
const atLastSnapPoint = snapPoints && activeSnapPointIndex === snapPoints.length - 1;
|
|
435
|
+
const restOffset = atLastSnapPoint ? (snapPointsOffset?.[activeSnapPointIndex!] ?? 0) : 0;
|
|
436
|
+
if (direction === 'bottom' ? swipeAmount > restOffset + 1 : swipeAmount < restOffset - 1) {
|
|
390
437
|
return true;
|
|
391
438
|
}
|
|
392
439
|
}
|
|
@@ -396,13 +443,16 @@ export function Root({
|
|
|
396
443
|
return false;
|
|
397
444
|
}
|
|
398
445
|
|
|
399
|
-
// Disallow dragging if drawer was scrolled within `scrollLockTimeout
|
|
446
|
+
// Disallow dragging if drawer was scrolled within `scrollLockTimeout`.
|
|
447
|
+
// Don't re-arm the timestamp here — a prevented drag attempt is not a
|
|
448
|
+
// scroll, and re-arming would keep the lock alive for as long as the
|
|
449
|
+
// finger moves, deadening the whole gesture instead of just the first
|
|
450
|
+
// `scrollLockTimeout` ms after the last real scroll.
|
|
400
451
|
if (
|
|
401
452
|
lastTimeDragPrevented.current &&
|
|
402
453
|
date.getTime() - lastTimeDragPrevented.current.getTime() < scrollLockTimeout &&
|
|
403
454
|
swipeAmount === 0
|
|
404
455
|
) {
|
|
405
|
-
lastTimeDragPrevented.current = date;
|
|
406
456
|
return false;
|
|
407
457
|
}
|
|
408
458
|
|
|
@@ -417,7 +467,11 @@ export function Root({
|
|
|
417
467
|
while (element) {
|
|
418
468
|
// Check if the element is scrollable
|
|
419
469
|
if (element.scrollHeight > element.clientHeight) {
|
|
420
|
-
|
|
470
|
+
// `> 0`, not `!== 0`: Safari reports a negative scrollTop during the
|
|
471
|
+
// rubber-band bounce at the top — that's "at the top" for drag
|
|
472
|
+
// purposes, and treating it as scrolled would arm the scroll lock and
|
|
473
|
+
// delay the sheet drag until the bounce fully settles.
|
|
474
|
+
if (element.scrollTop > 0) {
|
|
421
475
|
lastTimeDragPrevented.current = new Date();
|
|
422
476
|
|
|
423
477
|
// The element is scrollable and not scrolled to the top, so don't drag
|
|
@@ -480,10 +534,21 @@ export function Root({
|
|
|
480
534
|
return;
|
|
481
535
|
}
|
|
482
536
|
|
|
483
|
-
if (!isAllowedToDrag.current
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
537
|
+
if (!isAllowedToDrag.current) {
|
|
538
|
+
if (!shouldDrag(event.target, isDraggingInDirection)) return;
|
|
539
|
+
drawerRef.current.classList.add(DRAG_CLASS);
|
|
540
|
+
// If shouldDrag gave true once after pressing down on the drawer, we set isAllowedToDrag to true and it will remain true until we let go, there's no reason to disable dragging mid way, ever, and that's the solution to it
|
|
541
|
+
isAllowedToDrag.current = true;
|
|
542
|
+
// Touch pans latched onto a scroller would keep scrolling the content
|
|
543
|
+
// along with the sheet drag — freeze them for the drag's duration.
|
|
544
|
+
if (event.pointerType !== 'mouse') freezeScrollables(event.target);
|
|
545
|
+
// Drag can engage mid-gesture (content scrolled back to its top under
|
|
546
|
+
// the same finger, or the scroll lock expiring). Re-anchor and start
|
|
547
|
+
// moving on the next tick so the sheet tracks the finger from here
|
|
548
|
+
// instead of jumping by the distance the gesture already consumed.
|
|
549
|
+
pointerStart.current = isVertical(direction) ? event.pageY : event.pageX;
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
487
552
|
set(drawerRef.current, {
|
|
488
553
|
transition: 'none',
|
|
489
554
|
});
|
|
@@ -691,6 +756,7 @@ export function Root({
|
|
|
691
756
|
|
|
692
757
|
drawerRef.current.classList.remove(DRAG_CLASS);
|
|
693
758
|
isAllowedToDrag.current = false;
|
|
759
|
+
unfreezeScrollables();
|
|
694
760
|
setIsDragging(false);
|
|
695
761
|
dragEndTime.current = new Date();
|
|
696
762
|
}
|
|
@@ -700,6 +766,7 @@ export function Root({
|
|
|
700
766
|
|
|
701
767
|
drawerRef.current.classList.remove(DRAG_CLASS);
|
|
702
768
|
isAllowedToDrag.current = false;
|
|
769
|
+
unfreezeScrollables();
|
|
703
770
|
setIsDragging(false);
|
|
704
771
|
dragEndTime.current = new Date();
|
|
705
772
|
const swipeAmount = getTranslate(drawerRef.current, direction);
|
package/src/web/vaul/style.css
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
|
+
/* Registered so it can transition in sync with `transform` during snaps.
|
|
2
|
+
Descendants derive their visible-height layout from it, so `inherits: true`
|
|
3
|
+
is required. Unsupported browsers ignore this and the value just jumps to
|
|
4
|
+
the snap target instead of animating. */
|
|
5
|
+
@property --snap-point-height {
|
|
6
|
+
syntax: '<length>';
|
|
7
|
+
inherits: true;
|
|
8
|
+
initial-value: 0px;
|
|
9
|
+
}
|
|
10
|
+
|
|
1
11
|
[data-vaul-drawer] {
|
|
2
12
|
touch-action: none;
|
|
3
13
|
will-change: transform;
|
|
4
|
-
transition:
|
|
14
|
+
transition:
|
|
15
|
+
transform 0.5s cubic-bezier(0.32, 0.72, 0, 1),
|
|
16
|
+
--snap-point-height 0.5s cubic-bezier(0.32, 0.72, 0, 1);
|
|
5
17
|
animation-duration: 0.5s;
|
|
6
18
|
animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
|
|
7
19
|
}
|
|
@@ -84,6 +96,17 @@
|
|
|
84
96
|
transform: translate3d(var(--snap-point-height, 0), 0, 0);
|
|
85
97
|
}
|
|
86
98
|
|
|
99
|
+
/* Below the last snap point, vertical touch pans should move the sheet — not
|
|
100
|
+
scroll the content (matches native). touch-action is latched at gesture
|
|
101
|
+
start and ancestors above a scroller aren't consulted, so every descendant
|
|
102
|
+
(nested scrollers included) must opt out of vertical panning ahead of time.
|
|
103
|
+
pan-x keeps horizontal carousels inside the content working. Wheel/trackpad
|
|
104
|
+
scrolling is unaffected. */
|
|
105
|
+
[data-vaul-scroll-locked],
|
|
106
|
+
[data-vaul-scroll-locked] * {
|
|
107
|
+
touch-action: pan-x !important;
|
|
108
|
+
}
|
|
109
|
+
|
|
87
110
|
[data-vaul-overlay][data-vaul-snap-points='false'] {
|
|
88
111
|
animation-duration: 0.5s;
|
|
89
112
|
animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
|
|
@@ -198,13 +198,17 @@ export function useSnapPoints({
|
|
|
198
198
|
|
|
199
199
|
const animateThisSnap = hasSnappedRef.current || initialAnimated;
|
|
200
200
|
hasSnappedRef.current = true;
|
|
201
|
+
// `--snap-point-height` transitions alongside `transform` (registered via
|
|
202
|
+
// @property) so layouts derived from it (e.g. the scrollable fill) resize
|
|
203
|
+
// in sync with the drawer's slide instead of jumping to the target.
|
|
201
204
|
set(drawerRef.current, {
|
|
202
|
-
transition: animateThisSnap
|
|
203
|
-
? `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`
|
|
205
|
+
'transition': animateThisSnap
|
|
206
|
+
? `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')}), --snap-point-height ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`
|
|
204
207
|
: 'none',
|
|
205
|
-
transform: isVertical(direction)
|
|
208
|
+
'transform': isVertical(direction)
|
|
206
209
|
? `translate3d(0, ${dimension}px, 0)`
|
|
207
210
|
: `translate3d(${dimension}px, 0, 0)`,
|
|
211
|
+
'--snap-point-height': `${dimension}px`,
|
|
208
212
|
});
|
|
209
213
|
|
|
210
214
|
// Snapping implies drag overshoot (if any) should be undone.
|
|
@@ -360,19 +364,24 @@ export function useSnapPoints({
|
|
|
360
364
|
if ((direction === 'bottom' || direction === 'right') && newValue > snapPointsOffset[0]) {
|
|
361
365
|
const excess = newValue - snapPointsOffset[0];
|
|
362
366
|
set(drawerRef.current, {
|
|
363
|
-
transform: isVertical(direction)
|
|
367
|
+
'transform': isVertical(direction)
|
|
364
368
|
? `translate3d(0, ${snapPointsOffset[0]}px, 0)`
|
|
365
369
|
: `translate3d(${snapPointsOffset[0]}px, 0, 0)`,
|
|
370
|
+
'--snap-point-height': `${snapPointsOffset[0]}px`,
|
|
366
371
|
});
|
|
367
372
|
setDetachedWrapperTransform(excess, false);
|
|
368
373
|
return;
|
|
369
374
|
}
|
|
370
375
|
|
|
371
376
|
setDetachedWrapperTransform(0, false);
|
|
377
|
+
// Keep `--snap-point-height` tracking the live drag position so layouts
|
|
378
|
+
// derived from it (e.g. the scrollable fill) resize with the drawer
|
|
379
|
+
// instead of staying cut off at the last detent's visible height.
|
|
372
380
|
set(drawerRef.current, {
|
|
373
|
-
transform: isVertical(direction)
|
|
381
|
+
'transform': isVertical(direction)
|
|
374
382
|
? `translate3d(0, ${newValue}px, 0)`
|
|
375
383
|
: `translate3d(${newValue}px, 0, 0)`,
|
|
384
|
+
'--snap-point-height': `${newValue}px`,
|
|
376
385
|
});
|
|
377
386
|
}
|
|
378
387
|
|