react-modal-sheet 5.4.1 → 5.6.0
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 +263 -88
- package/dist/index.d.mts +16 -7
- package/dist/index.d.ts +16 -7
- package/dist/index.js +668 -610
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +664 -606
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +1 -1
- package/src/context.tsx +16 -6
- package/src/hooks/use-modal-effect.ts +87 -82
- package/src/index.tsx +11 -16
- package/src/{SheetBackdrop.tsx → sheet-backdrop.tsx} +3 -12
- package/src/{SheetContainer.tsx → sheet-container.tsx} +2 -2
- package/src/{SheetContent.tsx → sheet-content.tsx} +9 -3
- package/src/{SheetDragIndicator.tsx → sheet-drag-indicator.tsx} +2 -2
- package/src/{SheetHeader.tsx → sheet-header.tsx} +3 -3
- package/src/sheet.tsx +58 -85
- package/src/snap.ts +202 -103
- package/src/types.tsx +13 -1
- package/src/utils.ts +4 -0
package/src/sheet.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import React, {
|
|
11
11
|
forwardRef,
|
|
12
12
|
useImperativeHandle,
|
|
13
|
+
useMemo,
|
|
13
14
|
useRef,
|
|
14
15
|
useState,
|
|
15
16
|
} from 'react';
|
|
@@ -23,21 +24,21 @@ import {
|
|
|
23
24
|
IS_SSR,
|
|
24
25
|
REDUCED_MOTION_TWEEN_CONFIG,
|
|
25
26
|
} from './constants';
|
|
26
|
-
import {
|
|
27
|
+
import { ExposedSheetContext, InternalSheetContext } from './context';
|
|
27
28
|
import { useDimensions } from './hooks/use-dimensions';
|
|
28
29
|
import { useKeyboardAvoidance } from './hooks/use-keyboard-avoidance';
|
|
29
30
|
import { useModalEffect } from './hooks/use-modal-effect';
|
|
30
31
|
import { usePreventScroll } from './hooks/use-prevent-scroll';
|
|
31
32
|
import { useSheetState } from './hooks/use-sheet-state';
|
|
32
33
|
import { useStableCallback } from './hooks/use-stable-callback';
|
|
33
|
-
import {
|
|
34
|
-
computeSnapPoints,
|
|
35
|
-
handleHighVelocityDrag,
|
|
36
|
-
handleLowVelocityDrag,
|
|
37
|
-
} from './snap';
|
|
34
|
+
import { classifyDragEnd, computeSnapPoints } from './snap';
|
|
38
35
|
import { styles } from './styles';
|
|
39
|
-
import
|
|
40
|
-
|
|
36
|
+
import type {
|
|
37
|
+
ExposedContextType,
|
|
38
|
+
InternalContextType,
|
|
39
|
+
SheetProps,
|
|
40
|
+
} from './types';
|
|
41
|
+
import { applyStyles, clamp, waitForElement, willOpenKeyboard } from './utils';
|
|
41
42
|
|
|
42
43
|
export const Sheet = forwardRef<any, SheetProps>(
|
|
43
44
|
(
|
|
@@ -78,17 +79,12 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
78
79
|
const sheetRef = useRef<HTMLDivElement>(null);
|
|
79
80
|
const sheetHeight = Math.round(sheetBounds.height);
|
|
80
81
|
const [currentSnap, setCurrentSnap] = useState(initialSnap);
|
|
81
|
-
const snapPoints =
|
|
82
|
-
snapPointsProp && sheetHeight > 0
|
|
83
|
-
? computeSnapPoints({ sheetHeight, snapPointsProp })
|
|
84
|
-
: [];
|
|
85
|
-
|
|
86
82
|
const { windowHeight } = useDimensions();
|
|
87
83
|
const closedY = sheetHeight > 0 ? sheetHeight : windowHeight;
|
|
88
84
|
const y = useMotionValue(closedY);
|
|
89
85
|
const yInverted = useTransform(y, (val) => Math.max(sheetHeight - val, 0));
|
|
86
|
+
const yProgress = useTransform(y, (val) => clamp(1 - val / closedY, 0, 1));
|
|
90
87
|
const indicatorRotation = useMotionValue(0);
|
|
91
|
-
|
|
92
88
|
const shouldReduceMotion = useReducedMotion();
|
|
93
89
|
const reduceMotion = Boolean(prefersReducedMotion || shouldReduceMotion);
|
|
94
90
|
const animationOptions: Transition = {
|
|
@@ -104,6 +100,13 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
104
100
|
val + 2 >= closedY ? 'hidden' : 'visible'
|
|
105
101
|
);
|
|
106
102
|
|
|
103
|
+
// Memo snap points as we expose them in context so we want them to be stable
|
|
104
|
+
const snapPoints = useMemo(() => {
|
|
105
|
+
return snapPointsProp && sheetHeight > 0
|
|
106
|
+
? computeSnapPoints({ sheetHeight, snapPointsProp })
|
|
107
|
+
: [];
|
|
108
|
+
}, [sheetHeight, snapPointsProp?.join('')]);
|
|
109
|
+
|
|
107
110
|
const updateSnap = useStableCallback((snapIndex: number) => {
|
|
108
111
|
setCurrentSnap(snapIndex);
|
|
109
112
|
onSnap?.(snapIndex);
|
|
@@ -232,63 +235,31 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
232
235
|
|
|
233
236
|
const currentY = y.get();
|
|
234
237
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const isHighVelocity =
|
|
244
|
-
Math.abs(info.velocity.y) > dragVelocityThreshold;
|
|
238
|
+
const result = classifyDragEnd({
|
|
239
|
+
y: currentY,
|
|
240
|
+
info,
|
|
241
|
+
sheetHeight,
|
|
242
|
+
dragCloseThreshold,
|
|
243
|
+
snapPoints,
|
|
244
|
+
dragVelocityThreshold,
|
|
245
|
+
});
|
|
245
246
|
|
|
246
|
-
|
|
247
|
+
let yTo = result.yTo;
|
|
247
248
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
});
|
|
253
|
-
} else {
|
|
254
|
-
result = handleLowVelocityDrag({
|
|
255
|
-
currentSnapPoint,
|
|
256
|
-
currentY,
|
|
257
|
-
dragDirection: dragOffsetDirection,
|
|
258
|
-
snapPoints,
|
|
259
|
-
velocity: info.velocity.y,
|
|
260
|
-
});
|
|
261
|
-
}
|
|
249
|
+
// If disableDismiss is true, prevent closing via gesture
|
|
250
|
+
if (disableDismiss && yTo + 1 >= sheetHeight) {
|
|
251
|
+
// Use the bottom-most open snap point
|
|
252
|
+
const bottomSnapPoint = snapPoints.find((s) => s.snapValue > 0);
|
|
262
253
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (disableDismiss && yTo + 1 >= sheetHeight) {
|
|
267
|
-
// Use the bottom-most open snap point
|
|
268
|
-
const bottomSnapPoint = snapPoints.find((s) => s.snapValue > 0);
|
|
269
|
-
|
|
270
|
-
if (bottomSnapPoint) {
|
|
271
|
-
yTo = bottomSnapPoint.snapValueY;
|
|
272
|
-
updateSnap(bottomSnapPoint.snapIndex);
|
|
273
|
-
} else {
|
|
274
|
-
// If no open snap points available, stay at current position
|
|
275
|
-
yTo = currentY;
|
|
276
|
-
}
|
|
277
|
-
} else if (result.snapIndex !== undefined) {
|
|
278
|
-
updateSnap(result.snapIndex);
|
|
279
|
-
}
|
|
280
|
-
} else if (
|
|
281
|
-
info.velocity.y > dragVelocityThreshold ||
|
|
282
|
-
currentY > sheetHeight * dragCloseThreshold
|
|
283
|
-
) {
|
|
284
|
-
// Close the sheet if dragged past the threshold or if the velocity is high enough
|
|
285
|
-
// But only if disableDismiss is false
|
|
286
|
-
if (disableDismiss) {
|
|
287
|
-
// If disableDismiss, snap back to the open position
|
|
288
|
-
yTo = 0;
|
|
254
|
+
if (bottomSnapPoint) {
|
|
255
|
+
yTo = bottomSnapPoint.snapValueY;
|
|
256
|
+
updateSnap(bottomSnapPoint.snapIndex);
|
|
289
257
|
} else {
|
|
290
|
-
|
|
258
|
+
// If no open snap points available, stay at current position
|
|
259
|
+
yTo = currentY;
|
|
291
260
|
}
|
|
261
|
+
} else if (result.snapIndex !== undefined) {
|
|
262
|
+
updateSnap(result.snapIndex);
|
|
292
263
|
}
|
|
293
264
|
|
|
294
265
|
// Update the spring value so that the sheet is animated to the snap point
|
|
@@ -304,13 +275,6 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
304
275
|
indicatorRotation.set(0);
|
|
305
276
|
});
|
|
306
277
|
|
|
307
|
-
useImperativeHandle(ref, () => ({
|
|
308
|
-
y,
|
|
309
|
-
yInverted,
|
|
310
|
-
height: sheetHeight,
|
|
311
|
-
snapTo,
|
|
312
|
-
}));
|
|
313
|
-
|
|
314
278
|
useModalEffect({
|
|
315
279
|
y,
|
|
316
280
|
detent,
|
|
@@ -346,23 +310,17 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
346
310
|
const yTo = initialSnapPoint?.snapValueY ?? 0;
|
|
347
311
|
|
|
348
312
|
await animate(y, yTo, animationOptions);
|
|
349
|
-
|
|
350
|
-
if (initialSnap !== undefined) {
|
|
351
|
-
updateSnap(initialSnap);
|
|
352
|
-
}
|
|
353
|
-
|
|
313
|
+
if (initialSnap !== undefined) updateSnap(initialSnap);
|
|
354
314
|
onOpenEnd?.();
|
|
355
315
|
},
|
|
356
316
|
onClosing: async () => {
|
|
357
317
|
onCloseStart?.();
|
|
358
|
-
|
|
359
318
|
await animate(y, closedY, animationOptions);
|
|
360
|
-
|
|
361
319
|
onCloseEnd?.();
|
|
362
320
|
},
|
|
363
321
|
});
|
|
364
322
|
|
|
365
|
-
const dragProps:
|
|
323
|
+
const dragProps: InternalContextType['dragProps'] = {
|
|
366
324
|
drag: 'y',
|
|
367
325
|
dragElastic: 0,
|
|
368
326
|
dragMomentum: false,
|
|
@@ -372,7 +330,7 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
372
330
|
onDragEnd,
|
|
373
331
|
};
|
|
374
332
|
|
|
375
|
-
const
|
|
333
|
+
const internalContext: InternalContextType = {
|
|
376
334
|
currentSnap,
|
|
377
335
|
detent,
|
|
378
336
|
disableDrag,
|
|
@@ -383,11 +341,24 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
383
341
|
sheetBoundsRef,
|
|
384
342
|
sheetRef,
|
|
385
343
|
unstyled,
|
|
344
|
+
yProgress,
|
|
386
345
|
y,
|
|
387
346
|
};
|
|
388
347
|
|
|
348
|
+
const exposedContext: ExposedContextType = {
|
|
349
|
+
height: sheetHeight,
|
|
350
|
+
currentSnap,
|
|
351
|
+
snapPoints,
|
|
352
|
+
snapTo,
|
|
353
|
+
y,
|
|
354
|
+
yInverted,
|
|
355
|
+
yProgress,
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
useImperativeHandle(ref, () => exposedContext);
|
|
359
|
+
|
|
389
360
|
const sheet = (
|
|
390
|
-
<
|
|
361
|
+
<InternalSheetContext.Provider value={internalContext}>
|
|
391
362
|
<motion.div
|
|
392
363
|
{...rest}
|
|
393
364
|
ref={ref}
|
|
@@ -400,9 +371,11 @@ export const Sheet = forwardRef<any, SheetProps>(
|
|
|
400
371
|
...style,
|
|
401
372
|
}}
|
|
402
373
|
>
|
|
403
|
-
{
|
|
374
|
+
<ExposedSheetContext.Provider value={exposedContext}>
|
|
375
|
+
{state !== 'closed' ? children : null}
|
|
376
|
+
</ExposedSheetContext.Provider>
|
|
404
377
|
</motion.div>
|
|
405
|
-
</
|
|
378
|
+
</InternalSheetContext.Provider>
|
|
406
379
|
);
|
|
407
380
|
|
|
408
381
|
if (IS_SSR) return sheet;
|
package/src/snap.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { PanInfo } from 'motion';
|
|
1
2
|
import type { SheetSnapPoint } from './types';
|
|
2
|
-
import { isAscendingOrder } from './utils';
|
|
3
|
+
import { clamp, isAscendingOrder } from './utils';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Convert negative / percentage snap points to absolute values
|
|
@@ -107,136 +108,234 @@ export function computeSnapPoints({
|
|
|
107
108
|
}));
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
111
|
+
function sign(value: number) {
|
|
112
|
+
if (value > 0) return 1;
|
|
113
|
+
if (value < 0) return -1;
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function toAscendingYSnaps(snapPoints: SheetSnapPoint[]) {
|
|
118
|
+
return snapPoints.slice().sort((a, b) => a.snapValueY - b.snapValueY);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function findNearestSnapByY(snapPoints: SheetSnapPoint[], y: number) {
|
|
117
122
|
return snapPoints.reduce((closest, snap) =>
|
|
118
|
-
Math.abs(snap.snapValueY -
|
|
119
|
-
Math.abs(closest.snapValueY - currentY)
|
|
123
|
+
Math.abs(snap.snapValueY - y) < Math.abs(closest.snapValueY - y)
|
|
120
124
|
? snap
|
|
121
125
|
: closest
|
|
122
126
|
);
|
|
123
127
|
}
|
|
124
128
|
|
|
125
|
-
function
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
dragDirection: 'up' | 'down';
|
|
133
|
-
}) {
|
|
134
|
-
// NOTE: lower Y means higher in the sheet position!
|
|
135
|
-
if (dragDirection === 'down') {
|
|
136
|
-
/**
|
|
137
|
-
* Example:
|
|
138
|
-
*
|
|
139
|
-
* [
|
|
140
|
-
* { snapIndex: 0, snapValueY: 810 },
|
|
141
|
-
* { snapIndex: 1, snapValueY: 640 },
|
|
142
|
-
* { snapIndex: 2, snapValueY: 405 }, <-- next down
|
|
143
|
-
* ------------- Y = 60 ------------
|
|
144
|
-
* { snapIndex: 3, snapValueY: 50 },
|
|
145
|
-
* { snapIndex: 4, snapValueY: 0 },
|
|
146
|
-
* ]
|
|
147
|
-
*/
|
|
148
|
-
return snapPoints
|
|
149
|
-
.slice()
|
|
150
|
-
.reverse()
|
|
151
|
-
.find((s) => s.snapValueY > y);
|
|
152
|
-
} else {
|
|
153
|
-
/**
|
|
154
|
-
* Example:
|
|
155
|
-
* [
|
|
156
|
-
* { snapIndex: 0, snapValueY: 810 },
|
|
157
|
-
* { snapIndex: 1, snapValueY: 640 },
|
|
158
|
-
* { snapIndex: 2, snapValueY: 405 },
|
|
159
|
-
* ------------- Y = 60 ------------
|
|
160
|
-
* { snapIndex: 3, snapValueY: 50 }, <-- next up
|
|
161
|
-
* { snapIndex: 4, snapValueY: 0 },
|
|
162
|
-
* ]
|
|
163
|
-
*/
|
|
164
|
-
return snapPoints.find((s) => s.snapValueY < y);
|
|
129
|
+
function findNearestSnapAboveY(snapPoints: SheetSnapPoint[], y: number) {
|
|
130
|
+
const snaps = toAscendingYSnaps(snapPoints);
|
|
131
|
+
|
|
132
|
+
for (let i = snaps.length - 1; i >= 0; i--) {
|
|
133
|
+
if (snaps[i].snapValueY < y) {
|
|
134
|
+
return snaps[i];
|
|
135
|
+
}
|
|
165
136
|
}
|
|
137
|
+
|
|
138
|
+
return null;
|
|
166
139
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
snapPoints
|
|
140
|
+
|
|
141
|
+
function findNearestSnapBelowY(snapPoints: SheetSnapPoint[], y: number) {
|
|
142
|
+
const snaps = toAscendingYSnaps(snapPoints);
|
|
143
|
+
|
|
144
|
+
for (let i = 0; i < snaps.length; i++) {
|
|
145
|
+
if (snaps[i].snapValueY > y) {
|
|
146
|
+
return snaps[i];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function resolveDirection({
|
|
154
|
+
offsetY,
|
|
155
|
+
velocityY,
|
|
156
|
+
deltaY,
|
|
170
157
|
}: {
|
|
171
|
-
|
|
172
|
-
|
|
158
|
+
offsetY: number;
|
|
159
|
+
velocityY: number;
|
|
160
|
+
deltaY: number;
|
|
173
161
|
}) {
|
|
174
|
-
|
|
175
|
-
const bottomSnapPoint = snapPoints[0];
|
|
176
|
-
const topSnapPoint = snapPoints[snapPoints.length - 1];
|
|
162
|
+
if (Math.abs(velocityY) > 50) return sign(velocityY);
|
|
177
163
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
return {
|
|
185
|
-
yTo: topSnapPoint.snapValueY,
|
|
186
|
-
snapIndex: topSnapPoint.snapIndex,
|
|
187
|
-
};
|
|
164
|
+
const offsetDirection = sign(offsetY);
|
|
165
|
+
if (offsetDirection !== 0) return offsetDirection;
|
|
166
|
+
|
|
167
|
+
return sign(deltaY);
|
|
188
168
|
}
|
|
189
169
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Classifies the final sheet position when a drag gesture ends.
|
|
172
|
+
*
|
|
173
|
+
* Decision flow (high level):
|
|
174
|
+
* 1) Predict momentum endpoint from current position and velocity.
|
|
175
|
+
* 2) Resolve drag direction (prefer velocity, fallback to offset/delta).
|
|
176
|
+
* 3) Detect flick tiers:
|
|
177
|
+
* - strong flick -> jump fully open/closed
|
|
178
|
+
* - flick -> move to next snap in flick direction
|
|
179
|
+
* 4) For slower drags:
|
|
180
|
+
* - tiny drag or near-snap stickiness -> snap back to nearest snap
|
|
181
|
+
* - otherwise, use progress between surrounding snaps and commit threshold
|
|
182
|
+
* 5) Apply velocity-assisted bias to nearest predicted snap for natural feel.
|
|
183
|
+
* 6) Fallback to nearest snap.
|
|
184
|
+
*
|
|
185
|
+
* Special case with no snap points:
|
|
186
|
+
* - flick: open/close by direction
|
|
187
|
+
* - otherwise: open/close by predicted position vs close threshold midpoint
|
|
188
|
+
*/
|
|
189
|
+
export function classifyDragEnd({
|
|
190
|
+
y,
|
|
191
|
+
info,
|
|
192
|
+
sheetHeight,
|
|
194
193
|
snapPoints,
|
|
195
|
-
|
|
194
|
+
dragVelocityThreshold,
|
|
195
|
+
dragCloseThreshold,
|
|
196
196
|
}: {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
y: number;
|
|
198
|
+
info: PanInfo;
|
|
199
|
+
sheetHeight: number;
|
|
200
200
|
snapPoints: SheetSnapPoint[];
|
|
201
|
-
|
|
201
|
+
dragVelocityThreshold: number;
|
|
202
|
+
dragCloseThreshold: number;
|
|
202
203
|
}) {
|
|
203
|
-
const closestSnapRelativeToCurrentY = findClosestSnapPoint({
|
|
204
|
-
snapPoints,
|
|
205
|
-
currentY,
|
|
206
|
-
});
|
|
207
|
-
|
|
208
204
|
/**
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
* "momentum" that would push the sheet further to the given direction
|
|
205
|
+
* Thresholds and configuration for drag classification.
|
|
206
|
+
* Tweak these to adjust the feel of the sheet.
|
|
212
207
|
*/
|
|
213
|
-
|
|
208
|
+
// Velocity above this is considered a flick gesture.
|
|
209
|
+
const flickVelocity = dragVelocityThreshold;
|
|
210
|
+
// Velocity above this is treated as a strong flick and jumps to full open/closed.
|
|
211
|
+
const strongFlickVelocity = Math.max(2000, flickVelocity);
|
|
212
|
+
// In slow drags, crossing this portion of the current snap region commits to the next snap.
|
|
213
|
+
const distanceCommitRatio = 0.35;
|
|
214
|
+
// Time horizon (in seconds) used to estimate momentum-based end position.
|
|
215
|
+
const predictionTime = 0.2;
|
|
216
|
+
// Very small drags below this distance snap back instead of changing snap state.
|
|
217
|
+
const minDragDistance = 20;
|
|
218
|
+
// If drag ends within this many pixels from a snap, stick to that snap to reduce jitter.
|
|
219
|
+
const snapStickiness = 24;
|
|
220
|
+
|
|
221
|
+
const minY = 0;
|
|
222
|
+
const maxY = sheetHeight;
|
|
223
|
+
const offsetY = info.offset.y;
|
|
224
|
+
const deltaY = info.delta.y;
|
|
225
|
+
const velocityY = info.velocity.y;
|
|
226
|
+
|
|
227
|
+
// Step 1 — Compute predicted end position from momentum.
|
|
228
|
+
const predictedY = clamp(y + velocityY * predictionTime, minY, maxY);
|
|
229
|
+
const absVelocity = Math.abs(velocityY);
|
|
230
|
+
|
|
231
|
+
// Step 2 + 3 — Flick detection and gesture direction resolution.
|
|
232
|
+
const direction = resolveDirection({ offsetY, velocityY, deltaY });
|
|
233
|
+
|
|
234
|
+
const isStrongFlick = absVelocity > strongFlickVelocity;
|
|
235
|
+
const isFlick = absVelocity > flickVelocity;
|
|
236
|
+
|
|
237
|
+
// Step 4 — No snap points: decide between open/close only.
|
|
238
|
+
if (snapPoints.length === 0) {
|
|
239
|
+
if (isFlick) {
|
|
240
|
+
return {
|
|
241
|
+
yTo: direction < 0 ? minY : maxY,
|
|
242
|
+
snapIndex: undefined,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const midpoint = minY + (maxY - minY) * dragCloseThreshold;
|
|
247
|
+
|
|
214
248
|
return {
|
|
215
|
-
yTo:
|
|
216
|
-
snapIndex:
|
|
249
|
+
yTo: predictedY < midpoint ? minY : maxY,
|
|
250
|
+
snapIndex: undefined,
|
|
217
251
|
};
|
|
218
252
|
}
|
|
219
253
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
254
|
+
const nearestSnap = findNearestSnapByY(snapPoints, y);
|
|
255
|
+
|
|
256
|
+
// Slow-drag guard — tiny movement: snap back to nearest.
|
|
257
|
+
if (!isFlick && Math.abs(offsetY) < minDragDistance) {
|
|
258
|
+
return {
|
|
259
|
+
yTo: nearestSnap.snapValueY,
|
|
260
|
+
snapIndex: nearestSnap.snapIndex,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Hysteresis (stickiness) — avoid jitter if already close to a snap.
|
|
265
|
+
if (!isFlick && Math.abs(y - nearestSnap.snapValueY) < snapStickiness) {
|
|
266
|
+
return {
|
|
267
|
+
yTo: nearestSnap.snapValueY,
|
|
268
|
+
snapIndex: nearestSnap.snapIndex,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
229
271
|
|
|
230
|
-
|
|
272
|
+
// Step 5 — Strong flick overrides everything and goes fully open/closed.
|
|
273
|
+
if (isStrongFlick) {
|
|
274
|
+
const target = direction < 0 ? minY : maxY;
|
|
275
|
+
const targetSnap = findNearestSnapByY(snapPoints, target);
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
yTo: target,
|
|
279
|
+
snapIndex: targetSnap.snapIndex,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Step 5 — Medium flick moves to the next snap in flick direction.
|
|
284
|
+
if (isFlick) {
|
|
285
|
+
const nextSnap =
|
|
286
|
+
direction < 0
|
|
287
|
+
? findNearestSnapAboveY(snapPoints, y)
|
|
288
|
+
: findNearestSnapBelowY(snapPoints, y);
|
|
289
|
+
|
|
290
|
+
const fallbackY = direction < 0 ? minY : maxY;
|
|
291
|
+
const fallbackSnap = findNearestSnapByY(snapPoints, fallbackY);
|
|
292
|
+
const targetSnap = nextSnap ?? fallbackSnap;
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
yTo: targetSnap.snapValueY,
|
|
296
|
+
snapIndex: targetSnap.snapIndex,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const firstSnap = toAscendingYSnaps(snapPoints)[0];
|
|
301
|
+
const lastSnap = toAscendingYSnaps(snapPoints)[snapPoints.length - 1];
|
|
302
|
+
const prevSnap = findNearestSnapAboveY(snapPoints, y) ?? firstSnap;
|
|
303
|
+
const nextSnap = findNearestSnapBelowY(snapPoints, y) ?? lastSnap;
|
|
304
|
+
|
|
305
|
+
if (prevSnap.snapIndex === nextSnap.snapIndex) {
|
|
231
306
|
return {
|
|
232
|
-
yTo:
|
|
233
|
-
snapIndex:
|
|
307
|
+
yTo: prevSnap.snapValueY,
|
|
308
|
+
snapIndex: prevSnap.snapIndex,
|
|
234
309
|
};
|
|
235
310
|
}
|
|
236
311
|
|
|
237
|
-
|
|
312
|
+
const range = nextSnap.snapValueY - prevSnap.snapValueY;
|
|
313
|
+
const progress = range > 0 ? (y - prevSnap.snapValueY) / range : 0;
|
|
314
|
+
|
|
315
|
+
// Step 6 — Slow drag commit based on progress within the current snap region.
|
|
316
|
+
let selectedSnap = nearestSnap;
|
|
317
|
+
|
|
318
|
+
if (direction > 0) {
|
|
319
|
+
selectedSnap = progress > distanceCommitRatio ? nextSnap : prevSnap;
|
|
320
|
+
} else if (direction < 0) {
|
|
321
|
+
selectedSnap = progress < 1 - distanceCommitRatio ? prevSnap : nextSnap;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Step 7 — Velocity-assisted bias toward predicted-position snap.
|
|
325
|
+
if (absVelocity > 200) {
|
|
326
|
+
const predictedSnap = findNearestSnapByY(snapPoints, predictedY);
|
|
327
|
+
|
|
328
|
+
if (
|
|
329
|
+
Math.abs(predictedSnap.snapValueY - y) <
|
|
330
|
+
Math.abs(selectedSnap.snapValueY - y)
|
|
331
|
+
) {
|
|
332
|
+
selectedSnap = predictedSnap;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Step 8 — Final fallback.
|
|
238
337
|
return {
|
|
239
|
-
yTo:
|
|
240
|
-
snapIndex:
|
|
338
|
+
yTo: selectedSnap.snapValueY,
|
|
339
|
+
snapIndex: selectedSnap.snapIndex,
|
|
241
340
|
};
|
|
242
341
|
}
|
package/src/types.tsx
CHANGED
|
@@ -103,7 +103,7 @@ export type SheetSnapPoint = {
|
|
|
103
103
|
snapValueY: number; // Y value is inverted as `y = 0` means sheet is at the top
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
-
export interface
|
|
106
|
+
export interface InternalContextType {
|
|
107
107
|
currentSnap?: number;
|
|
108
108
|
detent: SheetDetent;
|
|
109
109
|
disableDrag: boolean;
|
|
@@ -114,9 +114,20 @@ export interface SheetContextType {
|
|
|
114
114
|
sheetBoundsRef: (node: HTMLDivElement | null) => void;
|
|
115
115
|
sheetRef: RefObject<any>;
|
|
116
116
|
unstyled: boolean;
|
|
117
|
+
yProgress: MotionValue<number>;
|
|
117
118
|
y: MotionValue<any>;
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
export interface ExposedContextType {
|
|
122
|
+
currentSnap?: number;
|
|
123
|
+
height: number;
|
|
124
|
+
snapPoints: SheetSnapPoint[];
|
|
125
|
+
snapTo: (index: number) => void;
|
|
126
|
+
y: MotionValue<number>;
|
|
127
|
+
yInverted: MotionValue<number>;
|
|
128
|
+
yProgress: MotionValue<number>;
|
|
129
|
+
}
|
|
130
|
+
|
|
120
131
|
export interface SheetScrollerContextType {
|
|
121
132
|
disableDrag: boolean;
|
|
122
133
|
setDragDisabled: () => void;
|
|
@@ -149,6 +160,7 @@ interface SheetCompoundComponent {
|
|
|
149
160
|
DragIndicator: FunctionComponent<SheetDragIndicatorProps>;
|
|
150
161
|
Content: ContentComponent;
|
|
151
162
|
Backdrop: BackdropComponent;
|
|
163
|
+
useContext: () => ExposedContextType;
|
|
152
164
|
}
|
|
153
165
|
|
|
154
166
|
export type SheetCompound = SheetComponent & SheetCompoundComponent;
|
package/src/utils.ts
CHANGED
|
@@ -114,3 +114,7 @@ export function willOpenKeyboard(target: Element) {
|
|
|
114
114
|
export function isHTTPS() {
|
|
115
115
|
return typeof window !== 'undefined' && window.isSecureContext;
|
|
116
116
|
}
|
|
117
|
+
|
|
118
|
+
export function clamp(value: number, min: number, max: number) {
|
|
119
|
+
return Math.min(Math.max(value, min), max);
|
|
120
|
+
}
|