Skip to content

Commit 4903634

Browse files
committed
wip
1 parent 54dda3a commit 4903634

10 files changed

Lines changed: 172 additions & 35 deletions

File tree

src/SheetBackdrop.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ export const SheetBackdrop = forwardRef<any, SheetBackdropProps>(
2525

2626
return (
2727
<Comp
28-
{...(rest as any)}
2928
ref={ref}
3029
className={`react-modal-sheet-backdrop ${className}`}
3130
style={backdropStyle}
3231
initial={{ opacity: 0 }}
3332
animate={{ opacity: 1 }}
3433
exit={{ opacity: 0 }}
3534
transition={{ duration: 1 }}
35+
{...(rest as any)}
3636
/>
3737
);
3838
}

src/SheetContainer.tsx

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,64 @@
1-
import { type MotionStyle, motion } from 'motion/react';
1+
import {
2+
type MotionStyle,
3+
motion,
4+
useMotionValueEvent,
5+
useTransform,
6+
} from 'motion/react';
27
import React, { forwardRef } from 'react';
38

49
import { DEFAULT_HEIGHT } from './constants';
510
import { useSheetContext } from './context';
611
import { styles } from './styles';
712
import { type SheetContainerProps } from './types';
813
import { applyStyles, mergeRefs } from './utils';
14+
import { useDimensions } from './hooks/use-dimensions';
915

1016
export const SheetContainer = forwardRef<any, SheetContainerProps>(
1117
({ children, style, className = '', unstyled, ...rest }, ref) => {
1218
const sheetContext = useSheetContext();
1319

1420
const isUnstyled = unstyled ?? sheetContext.unstyled;
1521

22+
const sheetHeightConstraint = sheetContext.sheetHeightConstraint;
23+
24+
useMotionValueEvent(sheetContext.yOverflow, 'change', (val) => {
25+
sheetContext.sheetRef.current?.style.setProperty(
26+
'--overflow',
27+
val + 'px'
28+
);
29+
});
30+
31+
// y might be negative due to elastic
32+
// for a better experience, we clamp the y value to 0
33+
// and use the overflow value to add padding to the bottom of the container
34+
// causing the illusion of the sheet being elastic
35+
const y = sheetContext.y;
36+
const nonNegativeY = useTransform(sheetContext.y, (val) =>
37+
Math.max(0, val)
38+
);
39+
40+
const { windowHeight } = useDimensions();
41+
const didHitMaxHeight =
42+
windowHeight - sheetHeightConstraint <= sheetContext.sheetHeight;
43+
1644
const containerStyle: MotionStyle = {
1745
...applyStyles(styles.container, isUnstyled),
1846
...style,
19-
y: sheetContext.y,
47+
...(isUnstyled
48+
? {
49+
y,
50+
}
51+
: {
52+
y: nonNegativeY,
53+
// compensate height for the elastic behavior of the sheet
54+
...(!didHitMaxHeight && { paddingBottom: sheetContext.yOverflow }),
55+
}),
2056
};
2157

58+
const constrainedHeight = `calc(${DEFAULT_HEIGHT} - ${sheetHeightConstraint}px)`;
59+
2260
if (sheetContext.detent === 'default') {
23-
containerStyle.height = DEFAULT_HEIGHT;
61+
containerStyle.height = constrainedHeight;
2462
}
2563

2664
if (sheetContext.detent === 'full') {
@@ -30,7 +68,7 @@ export const SheetContainer = forwardRef<any, SheetContainerProps>(
3068

3169
if (sheetContext.detent === 'content') {
3270
containerStyle.height = 'auto';
33-
containerStyle.maxHeight = DEFAULT_HEIGHT;
71+
containerStyle.maxHeight = constrainedHeight;
3472
}
3573

3674
return (

src/SheetContent.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,6 @@ export const SheetContent = forwardRef<any, SheetContentProps>(
7171
'env(keyboard-inset-height, var(--keyboard-inset-height, 0px))';
7272
}
7373

74-
if (disableScroll) {
75-
scrollStyle.overflowY = 'hidden';
76-
}
77-
7874
return (
7975
<motion.div
8076
{...rest}
@@ -85,13 +81,17 @@ export const SheetContent = forwardRef<any, SheetContentProps>(
8581
dragConstraints={dragConstraints.ref}
8682
onMeasureDragConstraints={dragConstraints.onMeasure}
8783
>
88-
<motion.div
89-
ref={mergeRefs([scroll.ref, scrollRefProp])}
90-
style={scrollStyle}
91-
className="react-modal-sheet-content-scroller"
92-
>
93-
{children}
94-
</motion.div>
84+
{disableScroll ? (
85+
children
86+
) : (
87+
<motion.div
88+
ref={mergeRefs([scroll.ref, scrollRefProp])}
89+
style={scrollStyle}
90+
className="react-modal-sheet-content-scroller"
91+
>
92+
{children}
93+
</motion.div>
94+
)}
9595
</motion.div>
9696
);
9797
}

src/constants.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import type { SheetTweenConfig } from './types';
22

3-
export const DEFAULT_HEIGHT = 'calc(100% - env(safe-area-inset-top) - 34px)';
3+
export const DEFAULT_TOP_CONSTRAINT = 34;
4+
5+
export const DEFAULT_HEIGHT =
6+
'calc(var(--overflow, 0px) + 100% - env(safe-area-inset-top))';
47

58
export const IS_SSR = typeof window === 'undefined';
69

src/hooks/use-prevent-scroll.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ function preventScrollMobileSafari() {
158158
const onTouchStart = (e: TouchEvent) => {
159159
// Use `composedPath` to support shadow DOM.
160160
const target = e.composedPath()?.[0] as HTMLElement;
161+
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return;
161162

162163
// Store the nearest scrollable parent element from the element that the user touched.
163164
scrollable = getScrollParent(target, true);

src/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@ import { SheetContent } from './SheetContent';
66
import { SheetDragIndicator } from './SheetDragIndicator';
77
import { SheetHeader } from './SheetHeader';
88
import { Sheet as SheetBase } from './sheet';
9-
import type { SheetCompound } from './types';
9+
import type { SheetCompound, SheetSnapPoint } from './types';
10+
import { useScrollPosition } from './hooks/use-scroll-position';
1011

1112
export interface SheetRef {
1213
y: MotionValue<number>;
1314
yInverted: MotionValue<number>;
1415
height: number;
1516
snapTo: (index: number) => Promise<void>;
17+
currentSnap: number | undefined;
18+
getSnapPoint: (index: number) => SheetSnapPoint | null;
19+
snapPoints: SheetSnapPoint[];
1620
}
1721

1822
export const Sheet: SheetCompound = Object.assign(SheetBase, {
@@ -23,6 +27,8 @@ export const Sheet: SheetCompound = Object.assign(SheetBase, {
2327
Backdrop: SheetBackdrop,
2428
});
2529

30+
export { useScrollPosition };
31+
2632
// Export types
2733
export type {
2834
SheetBackdropProps,

src/sheet.tsx

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
animate,
3+
Axis,
34
type DragHandler,
45
motion,
56
type Transition,
@@ -19,6 +20,7 @@ import useMeasure from 'react-use-measure';
1920
import {
2021
DEFAULT_DRAG_CLOSE_THRESHOLD,
2122
DEFAULT_DRAG_VELOCITY_THRESHOLD,
23+
DEFAULT_TOP_CONSTRAINT,
2224
DEFAULT_TWEEN_CONFIG,
2325
IS_SSR,
2426
REDUCED_MOTION_TWEEN_CONFIG,
@@ -37,7 +39,7 @@ import {
3739
} from './snap';
3840
import { styles } from './styles';
3941
import { type SheetContextType, type SheetProps } from './types';
40-
import { applyStyles, waitForElement } from './utils';
42+
import { applyConstraints, applyStyles, waitForElement } from './utils';
4143

4244
export const Sheet = forwardRef<any, SheetProps>(
4345
(
@@ -61,6 +63,7 @@ export const Sheet = forwardRef<any, SheetProps>(
6163
style,
6264
tweenConfig = DEFAULT_TWEEN_CONFIG,
6365
unstyled = false,
66+
dragConstraints: dragConstraintsProp,
6467
onOpenStart,
6568
onOpenEnd,
6669
onClose,
@@ -83,9 +86,27 @@ export const Sheet = forwardRef<any, SheetProps>(
8386
? computeSnapPoints({ sheetHeight, snapPointsProp })
8487
: [];
8588

89+
// for default & content detents, the sheet height is constrained instead of the drag
90+
const sheetHeightConstraint =
91+
detent === 'full'
92+
? 0
93+
: (dragConstraintsProp?.min ?? DEFAULT_TOP_CONSTRAINT);
94+
95+
const dragBottomConstraint =
96+
(dragConstraintsProp?.max ?? Infinity) - sheetHeightConstraint;
97+
98+
const dragConstraints: Axis = {
99+
min: 0, // top constraint (applied through sheet height instead)
100+
max: dragBottomConstraint, // bottom constraint
101+
};
102+
86103
const { windowHeight } = useDimensions();
87104
const closedY = sheetHeight > 0 ? sheetHeight : windowHeight;
88105
const y = useMotionValue(closedY);
106+
const yUnconstrainedRef = useRef<number | undefined>(undefined);
107+
// y is below 0 when the sheet is overextended
108+
// this happens because the sheet is elastic and can be dragged beyond the full open position
109+
const yOverflow = useTransform(y, (val) => (val < 0 ? Math.abs(val) : 0));
89110
const yInverted = useTransform(y, (val) => Math.max(sheetHeight - val, 0));
90111
const indicatorRotation = useMotionValue(0);
91112

@@ -104,12 +125,12 @@ export const Sheet = forwardRef<any, SheetProps>(
104125
// Disable drag if the keyboard is open to avoid weird behavior
105126
const disableDrag = keyboard.isKeyboardOpen || disableDragProp;
106127

107-
// +2 for tolerance in case the animated value is slightly off
128+
// -10 for tolerance in case the animated value is slightly off
108129
const zIndex = useTransform(y, (val) =>
109-
val + 2 >= closedY ? -1 : (style?.zIndex ?? 9999)
130+
val - 10 >= closedY ? -1 : (style?.zIndex ?? 9999)
110131
);
111132
const visibility = useTransform(y, (val) =>
112-
val + 2 >= closedY ? 'hidden' : 'visible'
133+
val - 10 >= closedY ? 'hidden' : 'visible'
113134
);
114135

115136
const updateSnap = useStableCallback((snapIndex: number) => {
@@ -173,27 +194,37 @@ export const Sheet = forwardRef<any, SheetProps>(
173194
});
174195

175196
const onDrag = useStableCallback<DragHandler>((event, info) => {
176-
onDragProp?.(event, info);
197+
if (yUnconstrainedRef.current === undefined) return;
177198

178-
const currentY = y.get();
199+
onDragProp?.(event, info);
200+
if (event.defaultPrevented) return;
179201

180202
// Update drag indicator rotation based on drag velocity
181203
const velocity = y.getVelocity();
182204
if (velocity > 0) indicatorRotation.set(10);
183205
if (velocity < 0) indicatorRotation.set(-10);
184206

185-
// Make sure user cannot drag beyond the top of the sheet
186-
y.set(Math.max(currentY + info.delta.y, 0));
207+
const currentY = yUnconstrainedRef.current;
208+
const nextY = currentY + info.delta.y;
209+
yUnconstrainedRef.current = nextY;
210+
const constrainedY = applyConstraints(nextY, dragConstraints, {
211+
min: 0.1,
212+
max: 0.1,
213+
});
214+
y.set(constrainedY);
187215
});
188216

189217
const onDragStart = useStableCallback<DragHandler>((event, info) => {
190-
blurActiveInput();
218+
yUnconstrainedRef.current = y.get();
191219
onDragStartProp?.(event, info);
220+
if (event.defaultPrevented) return;
221+
blurActiveInput();
192222
});
193223

194224
const onDragEnd = useStableCallback<DragHandler>((event, info) => {
195-
blurActiveInput();
196225
onDragEndProp?.(event, info);
226+
if (event.defaultPrevented) return;
227+
blurActiveInput();
197228

198229
const currentY = y.get();
199230

@@ -258,6 +289,7 @@ export const Sheet = forwardRef<any, SheetProps>(
258289

259290
// Update the spring value so that the sheet is animated to the snap point
260291
animate(y, yTo, animationOptions);
292+
yUnconstrainedRef.current = undefined;
261293

262294
// +1px for imprecision tolerance
263295
// Only call onClose if disableDismiss is false or if we're actually closing
@@ -274,6 +306,9 @@ export const Sheet = forwardRef<any, SheetProps>(
274306
yInverted,
275307
height: sheetHeight,
276308
snapTo,
309+
getSnapPoint,
310+
snapPoints,
311+
currentSnap,
277312
}));
278313

279314
useModalEffect({
@@ -348,6 +383,9 @@ export const Sheet = forwardRef<any, SheetProps>(
348383
sheetRef,
349384
unstyled,
350385
y,
386+
yOverflow,
387+
sheetHeight,
388+
sheetHeightConstraint,
351389
};
352390

353391
const sheet = (

src/snap.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,29 @@ export function handleLowVelocityDrag({
234234
};
235235
}
236236

237-
// No snap point down, stay at current
238-
return {
237+
const noChangesResult = {
239238
yTo: currentSnapPoint.snapValueY,
240239
snapIndex: currentSnapPoint.snapIndex,
241240
};
241+
242+
switch (dragDirection) {
243+
case 'down': {
244+
const firstSnapPoint = snapPoints.at(0);
245+
// No snap point down, stay at current
246+
if (!firstSnapPoint) return noChangesResult;
247+
return {
248+
yTo: firstSnapPoint.snapValueY,
249+
snapIndex: firstSnapPoint.snapIndex,
250+
};
251+
}
252+
case 'up': {
253+
const lastSnapPoint = snapPoints.at(-1);
254+
// No snap point up, stay at current
255+
if (!lastSnapPoint) return noChangesResult;
256+
return {
257+
yTo: lastSnapPoint.snapValueY,
258+
snapIndex: lastSnapPoint.snapIndex,
259+
};
260+
}
261+
}
242262
}

0 commit comments

Comments
 (0)