@@ -4,11 +4,9 @@ import { trackDismissableElement } from "@zag-js/dismissable"
4
4
import { addDomEvent , getEventPoint , getEventTarget , raf } from "@zag-js/dom-query"
5
5
import { trapFocus } from "@zag-js/focus-trap"
6
6
import { preventBodyScroll } from "@zag-js/remove-scroll"
7
- import type { Point } from "@zag-js/types"
8
7
import * as dom from "./bottom-sheet.dom"
9
8
import type { BottomSheetSchema , ResolvedSnapPoint } from "./bottom-sheet.types"
10
- import { findClosestSnapPoint } from "./utils/find-closest-snap-point"
11
- import { getScrollInfo } from "./utils/get-scroll-info"
9
+ import { DragManager } from "./utils/drag-manager"
12
10
import { resolveSnapPoint } from "./utils/resolve-snap-point"
13
11
14
12
export const machine = createMachine < BottomSheetSchema > ( {
@@ -26,7 +24,7 @@ export const machine = createMachine<BottomSheetSchema>({
26
24
initialFocusEl,
27
25
snapPoints : [ 1 ] ,
28
26
defaultActiveSnapPoint : 1 ,
29
- swipeVelocityThreshold : 500 ,
27
+ swipeVelocityThreshold : 700 ,
30
28
closeThreshold : 0.25 ,
31
29
preventDragOnScroll : true ,
32
30
...props ,
@@ -35,9 +33,6 @@ export const machine = createMachine<BottomSheetSchema>({
35
33
36
34
context ( { bindable, prop } ) {
37
35
return {
38
- pointerStart : bindable < Point | null > ( ( ) => ( {
39
- defaultValue : null ,
40
- } ) ) ,
41
36
dragOffset : bindable < number | null > ( ( ) => ( {
42
37
defaultValue : null ,
43
38
} ) ) ,
@@ -54,15 +49,12 @@ export const machine = createMachine<BottomSheetSchema>({
54
49
contentHeight : bindable < number | null > ( ( ) => ( {
55
50
defaultValue : null ,
56
51
} ) ) ,
57
- lastPoint : bindable < Point | null > ( ( ) => ( {
58
- defaultValue : null ,
59
- } ) ) ,
60
- lastTimestamp : bindable < number | null > ( ( ) => ( {
61
- defaultValue : null ,
62
- } ) ) ,
63
- velocity : bindable < number | null > ( ( ) => ( {
64
- defaultValue : null ,
65
- } ) ) ,
52
+ }
53
+ } ,
54
+
55
+ refs ( ) {
56
+ return {
57
+ dragManager : new DragManager ( ) ,
66
58
}
67
59
} ,
68
60
@@ -94,7 +86,7 @@ export const machine = createMachine<BottomSheetSchema>({
94
86
} ,
95
87
96
88
on : {
97
- SET_ACTIVE_SNAP_POINT : {
89
+ "ACTIVE_SNAP_POINT.SET" : {
98
90
actions : [ "setActiveSnapPoint" ] ,
99
91
} ,
100
92
} ,
@@ -114,18 +106,28 @@ export const machine = createMachine<BottomSheetSchema>({
114
106
"CONTROLLED.CLOSE" : {
115
107
target : "closed" ,
116
108
} ,
117
- POINTER_DOWN : [
109
+ POINTER_DOWN : {
110
+ actions : [ "setPointerStart" ] ,
111
+ } ,
112
+ POINTER_MOVE : [
118
113
{
119
- actions : [ "setPointerStart" ] ,
114
+ guard : "isDragging" ,
115
+ actions : [ "setDragOffset" ] ,
120
116
} ,
121
- ] ,
122
- POINTER_MOVE : [
123
117
{
124
118
guard : "shouldStartDragging" ,
125
- target : "open:dragging" ,
119
+ actions : [ "setDragOffset" ] ,
126
120
} ,
127
121
] ,
128
122
POINTER_UP : [
123
+ {
124
+ guard : "shouldCloseOnSwipe" ,
125
+ target : "closing" ,
126
+ } ,
127
+ {
128
+ guard : "isDragging" ,
129
+ actions : [ "setClosestSnapPoint" , "clearPointerStart" , "clearDragOffset" ] ,
130
+ } ,
129
131
{
130
132
actions : [ "clearPointerStart" , "clearDragOffset" ] ,
131
133
} ,
@@ -143,28 +145,6 @@ export const machine = createMachine<BottomSheetSchema>({
143
145
} ,
144
146
} ,
145
147
146
- "open:dragging" : {
147
- effects : [ "trackDismissableElement" , "preventScroll" , "trapFocus" , "hideContentBelow" , "trackPointerMove" ] ,
148
- tags : [ "open" , "dragging" ] ,
149
- on : {
150
- POINTER_MOVE : [
151
- {
152
- actions : [ "setDragOffset" ] ,
153
- } ,
154
- ] ,
155
- POINTER_UP : [
156
- {
157
- guard : "shouldCloseOnSwipe" ,
158
- target : "closing" ,
159
- } ,
160
- {
161
- actions : [ "setClosestSnapPoint" , "clearPointerStart" , "clearDragOffset" ] ,
162
- target : "open" ,
163
- } ,
164
- ] ,
165
- } ,
166
- } ,
167
-
168
148
closing : {
169
149
effects : [ "trackExitAnimation" ] ,
170
150
on : {
@@ -207,51 +187,28 @@ export const machine = createMachine<BottomSheetSchema>({
207
187
guards : {
208
188
isOpenControlled : ( { prop } ) => prop ( "open" ) !== undefined ,
209
189
210
- shouldStartDragging ( { prop, context, event, scope, send } ) {
211
- const pointerStart = context . get ( "pointerStart" )
212
- const container = dom . getContentEl ( scope )
213
- if ( ! pointerStart || ! container ) return false
214
-
215
- const { point, target } = event
216
-
217
- if ( prop ( "preventDragOnScroll" ) ) {
218
- const delta = pointerStart . y - point . y
219
-
220
- if ( Math . abs ( delta ) < 0.3 ) return false
221
-
222
- const { availableScroll, availableScrollTop } = getScrollInfo ( target , container )
223
-
224
- if ( ( delta > 0 && Math . abs ( availableScroll ) > 1 ) || ( delta < 0 && Math . abs ( availableScrollTop ) > 0 ) ) {
225
- send ( { type : "POINTER_UP" , point } )
226
- return false
227
- }
228
- }
229
-
230
- return true
190
+ isDragging ( { context } ) {
191
+ return context . get ( "dragOffset" ) !== null
231
192
} ,
232
193
233
- shouldCloseOnSwipe ( { prop, context, computed } ) {
234
- const velocity = context . get ( "velocity" )
235
- const dragOffset = context . get ( "dragOffset" )
236
- const contentHeight = context . get ( "contentHeight" )
237
- const swipeVelocityThreshold = prop ( "swipeVelocityThreshold" )
238
- const closeThreshold = prop ( "closeThreshold" )
239
- const snapPoints = computed ( "resolvedSnapPoints" )
240
-
241
- if ( dragOffset === null || contentHeight === null || velocity === null ) return false
242
-
243
- const visibleHeight = contentHeight - dragOffset
244
- const smallestSnapPoint = snapPoints . reduce ( ( acc , curr ) => ( curr . offset > acc . offset ? curr : acc ) )
245
-
246
- const isFastSwipe = velocity > 0 && velocity >= swipeVelocityThreshold
247
-
248
- const closeThresholdInPixels = contentHeight * ( 1 - closeThreshold )
249
- const isBelowSmallestSnapPoint = visibleHeight < contentHeight - smallestSnapPoint . offset
250
- const isBelowCloseThreshold = visibleHeight < closeThresholdInPixels
251
-
252
- const hasEnoughDragToDismiss = ( isBelowCloseThreshold && isBelowSmallestSnapPoint ) || visibleHeight === 0
194
+ shouldStartDragging ( { prop, refs, event, scope } ) {
195
+ const dragManager = refs . get ( "dragManager" )
196
+ return dragManager . shouldStartDragging (
197
+ event . point ,
198
+ event . target ,
199
+ dom . getContentEl ( scope ) ,
200
+ prop ( "preventDragOnScroll" ) ,
201
+ )
202
+ } ,
253
203
254
- return isFastSwipe || hasEnoughDragToDismiss
204
+ shouldCloseOnSwipe ( { prop, context, computed, refs } ) {
205
+ const dragManager = refs . get ( "dragManager" )
206
+ return dragManager . shouldDismiss (
207
+ context . get ( "contentHeight" ) ,
208
+ computed ( "resolvedSnapPoints" ) ,
209
+ prop ( "swipeVelocityThreshold" ) ,
210
+ prop ( "closeThreshold" ) ,
211
+ )
255
212
} ,
256
213
} ,
257
214
@@ -268,53 +225,35 @@ export const machine = createMachine<BottomSheetSchema>({
268
225
context . set ( "activeSnapPoint" , event . snapPoint )
269
226
} ,
270
227
271
- setPointerStart ( { event, context } ) {
272
- context . set ( "pointerStart" , event . point )
228
+ setPointerStart ( { event, refs } ) {
229
+ refs . get ( "dragManager" ) . setPointerStart ( event . point )
273
230
} ,
274
231
275
- setDragOffset ( { context, event } ) {
276
- const pointerStart = context . get ( "pointerStart" )
277
- if ( ! pointerStart ) return
278
-
279
- const { point } = event
280
-
281
- const currentTimestamp = new Date ( ) . getTime ( )
282
-
283
- const lastPoint = context . get ( "lastPoint" )
284
- if ( lastPoint ) {
285
- const dy = point . y - lastPoint . y
286
-
287
- const lastTimestamp = context . get ( "lastTimestamp" )
288
- if ( lastTimestamp ) {
289
- const dt = currentTimestamp - lastTimestamp
290
- if ( dt > 0 ) {
291
- context . set ( "velocity" , ( dy / dt ) * 1000 )
292
- }
293
- }
294
- }
295
-
296
- context . set ( "lastPoint" , point )
297
- context . set ( "lastTimestamp" , currentTimestamp )
298
-
299
- let delta = pointerStart . y - point . y - ( context . get ( "resolvedActiveSnapPoint" ) ?. offset || 0 )
300
- if ( delta > 0 ) delta = 0
301
-
302
- context . set ( "dragOffset" , - delta )
232
+ setDragOffset ( { context, event, refs } ) {
233
+ const dragManager = refs . get ( "dragManager" )
234
+ dragManager . setDragOffset ( event . point , context . get ( "resolvedActiveSnapPoint" ) ?. offset || 0 )
235
+ context . set ( "dragOffset" , dragManager . getDragOffset ( ) )
303
236
} ,
304
237
305
- setClosestSnapPoint ( { computed, context } ) {
238
+ setClosestSnapPoint ( { computed, context, refs } ) {
306
239
const snapPoints = computed ( "resolvedSnapPoints" )
307
240
const contentHeight = context . get ( "contentHeight" )
308
- const dragOffset = context . get ( "dragOffset" )
309
241
310
- if ( ! snapPoints || contentHeight === null || dragOffset === null ) return
242
+ if ( ! snapPoints . length || contentHeight === null ) return
311
243
312
- const closestSnapPoint = findClosestSnapPoint ( dragOffset , snapPoints )
244
+ const dragManager = refs . get ( "dragManager" )
245
+ const closestSnapPoint = dragManager . findClosestSnapPoint ( snapPoints )
313
246
314
- context . set ( "activeSnapPoint" , closestSnapPoint . value )
247
+ // Set activeSnapPoint
248
+ context . set ( "activeSnapPoint" , closestSnapPoint )
249
+
250
+ // Also resolve and set immediately to prevent visual snap flash
251
+ const resolved = resolveSnapPoint ( closestSnapPoint , contentHeight )
252
+ context . set ( "resolvedActiveSnapPoint" , resolved )
315
253
} ,
316
254
317
- clearDragOffset ( { context } ) {
255
+ clearDragOffset ( { context, refs } ) {
256
+ refs . get ( "dragManager" ) . clearDragOffset ( )
318
257
context . set ( "dragOffset" , null )
319
258
} ,
320
259
@@ -326,18 +265,16 @@ export const machine = createMachine<BottomSheetSchema>({
326
265
context . set ( "resolvedActiveSnapPoint" , null )
327
266
} ,
328
267
329
- clearPointerStart ( { context } ) {
330
- context . set ( "pointerStart" , null )
268
+ clearPointerStart ( { refs } ) {
269
+ refs . get ( "dragManager" ) . clearPointerStart ( )
331
270
} ,
332
271
333
272
clearContentHeight ( { context } ) {
334
273
context . set ( "contentHeight" , null )
335
274
} ,
336
275
337
- clearVelocityTracking ( { context } ) {
338
- context . set ( "lastPoint" , null )
339
- context . set ( "lastTimestamp" , null )
340
- context . set ( "velocity" , null )
276
+ clearVelocityTracking ( { refs } ) {
277
+ refs . get ( "dragManager" ) . clearVelocityTracking ( )
341
278
} ,
342
279
343
280
toggleVisibility ( { event, send, prop } ) {
@@ -404,10 +341,9 @@ export const machine = createMachine<BottomSheetSchema>({
404
341
}
405
342
406
343
function onPointerUp ( event : PointerEvent ) {
407
- if ( event . pointerType !== "touch" ) {
408
- const point = getEventPoint ( event )
409
- send ( { type : "POINTER_UP" , point } )
410
- }
344
+ if ( event . pointerType === "touch" ) return
345
+ const point = getEventPoint ( event )
346
+ send ( { type : "POINTER_UP" , point } )
411
347
}
412
348
413
349
function onTouchStart ( event : TouchEvent ) {
@@ -428,6 +364,7 @@ export const machine = createMachine<BottomSheetSchema>({
428
364
// Prevent overscrolling
429
365
const contentEl = dom . getContentEl ( scope )
430
366
if ( ! contentEl ) return
367
+
431
368
let el : HTMLElement | null = target
432
369
while ( el && el !== contentEl && el . scrollHeight <= el . clientHeight ) {
433
370
el = el . parentElement
@@ -455,12 +392,14 @@ export const machine = createMachine<BottomSheetSchema>({
455
392
send ( { type : "POINTER_UP" , point } )
456
393
}
457
394
395
+ const doc = scope . getDoc ( )
396
+
458
397
const cleanups = [
459
- addDomEvent ( scope . getDoc ( ) , "pointermove" , onPointerMove ) ,
460
- addDomEvent ( scope . getDoc ( ) , "pointerup" , onPointerUp ) ,
461
- addDomEvent ( scope . getDoc ( ) , "touchstart" , onTouchStart , { passive : false } ) ,
462
- addDomEvent ( scope . getDoc ( ) , "touchmove" , onTouchMove , { passive : false } ) ,
463
- addDomEvent ( scope . getDoc ( ) , "touchend" , onTouchEnd ) ,
398
+ addDomEvent ( doc , "pointermove" , onPointerMove ) ,
399
+ addDomEvent ( doc , "pointerup" , onPointerUp ) ,
400
+ addDomEvent ( doc , "touchstart" , onTouchStart , { passive : false } ) ,
401
+ addDomEvent ( doc , "touchmove" , onTouchMove , { passive : false } ) ,
402
+ addDomEvent ( doc , "touchend" , onTouchEnd ) ,
464
403
]
465
404
466
405
return ( ) => {
0 commit comments