@@ -4,11 +4,9 @@ import { trackDismissableElement } from "@zag-js/dismissable"
44import { addDomEvent , getEventPoint , getEventTarget , raf } from "@zag-js/dom-query"
55import { trapFocus } from "@zag-js/focus-trap"
66import { preventBodyScroll } from "@zag-js/remove-scroll"
7- import type { Point } from "@zag-js/types"
87import * as dom from "./bottom-sheet.dom"
98import 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"
1210import { resolveSnapPoint } from "./utils/resolve-snap-point"
1311
1412export const machine = createMachine < BottomSheetSchema > ( {
@@ -26,7 +24,7 @@ export const machine = createMachine<BottomSheetSchema>({
2624 initialFocusEl,
2725 snapPoints : [ 1 ] ,
2826 defaultActiveSnapPoint : 1 ,
29- swipeVelocityThreshold : 500 ,
27+ swipeVelocityThreshold : 700 ,
3028 closeThreshold : 0.25 ,
3129 preventDragOnScroll : true ,
3230 ...props ,
@@ -35,9 +33,6 @@ export const machine = createMachine<BottomSheetSchema>({
3533
3634 context ( { bindable, prop } ) {
3735 return {
38- pointerStart : bindable < Point | null > ( ( ) => ( {
39- defaultValue : null ,
40- } ) ) ,
4136 dragOffset : bindable < number | null > ( ( ) => ( {
4237 defaultValue : null ,
4338 } ) ) ,
@@ -54,15 +49,12 @@ export const machine = createMachine<BottomSheetSchema>({
5449 contentHeight : bindable < number | null > ( ( ) => ( {
5550 defaultValue : null ,
5651 } ) ) ,
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 ( ) ,
6658 }
6759 } ,
6860
@@ -94,7 +86,7 @@ export const machine = createMachine<BottomSheetSchema>({
9486 } ,
9587
9688 on : {
97- SET_ACTIVE_SNAP_POINT : {
89+ "ACTIVE_SNAP_POINT.SET" : {
9890 actions : [ "setActiveSnapPoint" ] ,
9991 } ,
10092 } ,
@@ -114,18 +106,28 @@ export const machine = createMachine<BottomSheetSchema>({
114106 "CONTROLLED.CLOSE" : {
115107 target : "closed" ,
116108 } ,
117- POINTER_DOWN : [
109+ POINTER_DOWN : {
110+ actions : [ "setPointerStart" ] ,
111+ } ,
112+ POINTER_MOVE : [
118113 {
119- actions : [ "setPointerStart" ] ,
114+ guard : "isDragging" ,
115+ actions : [ "setDragOffset" ] ,
120116 } ,
121- ] ,
122- POINTER_MOVE : [
123117 {
124118 guard : "shouldStartDragging" ,
125- target : "open:dragging" ,
119+ actions : [ "setDragOffset" ] ,
126120 } ,
127121 ] ,
128122 POINTER_UP : [
123+ {
124+ guard : "shouldCloseOnSwipe" ,
125+ target : "closing" ,
126+ } ,
127+ {
128+ guard : "isDragging" ,
129+ actions : [ "setClosestSnapPoint" , "clearPointerStart" , "clearDragOffset" ] ,
130+ } ,
129131 {
130132 actions : [ "clearPointerStart" , "clearDragOffset" ] ,
131133 } ,
@@ -143,28 +145,6 @@ export const machine = createMachine<BottomSheetSchema>({
143145 } ,
144146 } ,
145147
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-
168148 closing : {
169149 effects : [ "trackExitAnimation" ] ,
170150 on : {
@@ -207,51 +187,28 @@ export const machine = createMachine<BottomSheetSchema>({
207187 guards : {
208188 isOpenControlled : ( { prop } ) => prop ( "open" ) !== undefined ,
209189
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
231192 } ,
232193
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+ } ,
253203
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+ )
255212 } ,
256213 } ,
257214
@@ -268,53 +225,35 @@ export const machine = createMachine<BottomSheetSchema>({
268225 context . set ( "activeSnapPoint" , event . snapPoint )
269226 } ,
270227
271- setPointerStart ( { event, context } ) {
272- context . set ( "pointerStart" , event . point )
228+ setPointerStart ( { event, refs } ) {
229+ refs . get ( "dragManager" ) . setPointerStart ( event . point )
273230 } ,
274231
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 ( ) )
303236 } ,
304237
305- setClosestSnapPoint ( { computed, context } ) {
238+ setClosestSnapPoint ( { computed, context, refs } ) {
306239 const snapPoints = computed ( "resolvedSnapPoints" )
307240 const contentHeight = context . get ( "contentHeight" )
308- const dragOffset = context . get ( "dragOffset" )
309241
310- if ( ! snapPoints || contentHeight === null || dragOffset === null ) return
242+ if ( ! snapPoints . length || contentHeight === null ) return
311243
312- const closestSnapPoint = findClosestSnapPoint ( dragOffset , snapPoints )
244+ const dragManager = refs . get ( "dragManager" )
245+ const closestSnapPoint = dragManager . findClosestSnapPoint ( snapPoints )
313246
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 )
315253 } ,
316254
317- clearDragOffset ( { context } ) {
255+ clearDragOffset ( { context, refs } ) {
256+ refs . get ( "dragManager" ) . clearDragOffset ( )
318257 context . set ( "dragOffset" , null )
319258 } ,
320259
@@ -326,18 +265,16 @@ export const machine = createMachine<BottomSheetSchema>({
326265 context . set ( "resolvedActiveSnapPoint" , null )
327266 } ,
328267
329- clearPointerStart ( { context } ) {
330- context . set ( "pointerStart" , null )
268+ clearPointerStart ( { refs } ) {
269+ refs . get ( "dragManager" ) . clearPointerStart ( )
331270 } ,
332271
333272 clearContentHeight ( { context } ) {
334273 context . set ( "contentHeight" , null )
335274 } ,
336275
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 ( )
341278 } ,
342279
343280 toggleVisibility ( { event, send, prop } ) {
@@ -404,10 +341,9 @@ export const machine = createMachine<BottomSheetSchema>({
404341 }
405342
406343 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 } )
411347 }
412348
413349 function onTouchStart ( event : TouchEvent ) {
@@ -428,6 +364,7 @@ export const machine = createMachine<BottomSheetSchema>({
428364 // Prevent overscrolling
429365 const contentEl = dom . getContentEl ( scope )
430366 if ( ! contentEl ) return
367+
431368 let el : HTMLElement | null = target
432369 while ( el && el !== contentEl && el . scrollHeight <= el . clientHeight ) {
433370 el = el . parentElement
@@ -455,12 +392,14 @@ export const machine = createMachine<BottomSheetSchema>({
455392 send ( { type : "POINTER_UP" , point } )
456393 }
457394
395+ const doc = scope . getDoc ( )
396+
458397 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 ) ,
464403 ]
465404
466405 return ( ) => {
0 commit comments