@@ -3,7 +3,7 @@ import { createGesture } from '@utils/gesture';
33import { clamp , getElementRoot , raf } from '@utils/helpers' ;
44import { FOCUS_TRAP_DISABLE_CLASS } from '@utils/overlays' ;
55
6- import type { Animation } from '../../../interface' ;
6+ import type { Animation , ModalDragEventDetail } from '../../../interface' ;
77import type { GestureDetail } from '../../../utils/gesture' ;
88import { getBackdropValueForSheet } from '../utils' ;
99
@@ -52,7 +52,10 @@ export const createSheetGesture = (
5252 expandToScroll : boolean ,
5353 getCurrentBreakpoint : ( ) => number ,
5454 onDismiss : ( ) => void ,
55- onBreakpointChange : ( breakpoint : number ) => void
55+ onBreakpointChange : ( breakpoint : number ) => void ,
56+ onDragStart : ( ) => void ,
57+ onDragMove : ( detail : ModalDragEventDetail ) => void ,
58+ onDragEnd : ( detail : ModalDragEventDetail ) => void
5659) => {
5760 // Defaults for the sheet swipe animation
5861 const defaultBackdrop = [
@@ -347,6 +350,8 @@ export const createSheetGesture = (
347350 } ) ;
348351
349352 animation . progressStart ( true , 1 - currentBreakpoint ) ;
353+
354+ onDragStart ( ) ;
350355 } ;
351356
352357 const onMove = ( detail : GestureDetail ) => {
@@ -423,9 +428,31 @@ export const createSheetGesture = (
423428
424429 offset = clamp ( 0.0001 , processedStep , maxStep ) ;
425430 animation . progressStep ( offset ) ;
431+
432+ const snapBreakpoint = calculateSnapBreakpoint ( detail . deltaY ) ;
433+
434+ const eventDetail : ModalDragEventDetail = {
435+ currentY : detail . currentY ,
436+ deltaY : detail . deltaY ,
437+ velocityY : detail . velocityY ,
438+ progress : calculateProgress ( detail . currentY ) ,
439+ snapBreakpoint : snapBreakpoint ,
440+ } ;
441+
442+ onDragMove ( eventDetail ) ;
426443 } ;
427444
428445 const onEnd = ( detail : GestureDetail ) => {
446+ const snapBreakpoint = calculateSnapBreakpoint ( detail . deltaY ) ;
447+
448+ const eventDetail : ModalDragEventDetail = {
449+ currentY : detail . currentY ,
450+ deltaY : detail . deltaY ,
451+ velocityY : detail . velocityY ,
452+ progress : calculateProgress ( detail . currentY ) ,
453+ snapBreakpoint,
454+ } ;
455+
429456 /**
430457 * If expandToScroll is disabled, we should not allow the moveSheetToBreakpoint
431458 * function to be called if the user is trying to swipe content upwards and the content
@@ -440,23 +467,13 @@ export const createSheetGesture = (
440467 * swap to moving on drag and if we don't swap back here then the footer will get stuck.
441468 */
442469 swapFooterPosition ( 'stationary' ) ;
470+ onDragEnd ( eventDetail ) ;
471+
443472 return ;
444473 }
445474
446- /**
447- * When the gesture releases, we need to determine
448- * the closest breakpoint to snap to.
449- */
450- const velocity = detail . velocityY ;
451- const threshold = ( detail . deltaY + velocity * 350 ) / height ;
452-
453- const diff = currentBreakpoint - threshold ;
454- const closest = breakpoints . reduce ( ( a , b ) => {
455- return Math . abs ( b - diff ) < Math . abs ( a - diff ) ? b : a ;
456- } ) ;
457-
458475 moveSheetToBreakpoint ( {
459- breakpoint : closest ,
476+ breakpoint : snapBreakpoint ,
460477 breakpointOffset : offset ,
461478 canDismiss : canDismissBlocksGesture ,
462479
@@ -466,6 +483,8 @@ export const createSheetGesture = (
466483 */
467484 animated : true ,
468485 } ) ;
486+
487+ onDragEnd ( eventDetail ) ;
469488 } ;
470489
471490 const moveSheetToBreakpoint = ( options : MoveSheetToBreakpointOptions ) => {
@@ -624,6 +643,112 @@ export const createSheetGesture = (
624643 } ) ;
625644 } ;
626645
646+ /**
647+ * Calculates the breakpoint based on the current deltaY.
648+ * This determines where the sheet should snap to when the user releases the
649+ * gesture.
650+ *
651+ * @param deltaY The change in Y position since the gesture started.
652+ * @returns The snap breakpoint value.
653+ */
654+ const calculateSnapBreakpoint = ( deltaY : number ) : number => {
655+ /**
656+ * Calculates the real-time vertical position of the modal.
657+ * We combine the wrapper's current bounding box position with the
658+ * gesture's deltaY to account for the physical movement during the drag.
659+ */
660+ const currentY = wrapperEl . getBoundingClientRect ( ) . top + deltaY ;
661+ /**
662+ * Convert that pixel position back into a 0 to 1 progress value.
663+ */
664+ const currentProgress = calculateProgress ( currentY ) ;
665+
666+ /**
667+ * Find and return the defined breakpoint that is closest to the
668+ * current progress.
669+ */
670+ const snapBreakpoint = breakpoints . reduce ( ( a , b ) => {
671+ return Math . abs ( b - currentProgress ) < Math . abs ( a - currentProgress ) ? b : a ;
672+ } ) ;
673+
674+ return snapBreakpoint ;
675+ } ;
676+
677+ /**
678+ * Calculates the progress of the swipe gesture.
679+ *
680+ * The progress is a value between 0 and 1 that represents how far
681+ * the swipe has progressed towards closing the modal.
682+ *
683+ * A value closer to 1 means the modal is closer to being opened,
684+ * while a value closer to 0 means the modal is closer to being closed.
685+ *
686+ * @param currentY The current Y position of the gesture
687+ * @returns The progress of the sheet gesture
688+ */
689+ const calculateProgress = ( currentY : number ) : number => {
690+ const minBreakpoint = breakpoints [ 0 ] ;
691+ const maxBreakpoint = breakpoints [ breakpoints . length - 1 ] ;
692+
693+ /**
694+ * The lowest point the sheet can be dragged to aka the point at which
695+ * the sheet is fully closed.
696+ */
697+ const maxY = convertBreakpointToY ( minBreakpoint ) ;
698+ /**
699+ * The highest point the sheet can be dragged to aka the point at which
700+ * the sheet is fully open.
701+ */
702+ const minY = convertBreakpointToY ( maxBreakpoint ) ;
703+ // The total distance between the fully open and fully closed positions.
704+ const totalDistance = maxY - minY ;
705+ // The distance from the current position to the fully closed position.
706+ const distanceFromBottom = maxY - currentY ;
707+ /**
708+ * The progress represents how far the sheet is from the bottom relative
709+ * to the total distance. When the user starts swiping up, the progress
710+ * should be close to 1, and when the user has swiped all the way down,
711+ * the progress should be close to 0.
712+ */
713+ const progress = distanceFromBottom / totalDistance ;
714+ // Round to the nearest thousandth to avoid returning very small decimal
715+ const roundedProgress = Math . round ( progress * 1000 ) / 1000 ;
716+
717+ return Math . max ( 0 , Math . min ( 1 , roundedProgress ) ) ;
718+ } ;
719+
720+ /**
721+ * Converts a breakpoint value (0 to 1) into a pixel Y coordinate
722+ * on the screen.
723+ *
724+ * @param breakpoint The breakpoint value (e.g., 0.5 for half-open)
725+ * @returns The pixel Y coordinate on the screen
726+ */
727+ const convertBreakpointToY = ( breakpoint : number ) : number => {
728+ const rect = baseEl . getBoundingClientRect ( ) ;
729+ const modalHeight = rect . height ;
730+ // The bottom of the screen.
731+ const viewportBottom = window . innerHeight ;
732+ /**
733+ * The active height is how much of the modal is actually showing
734+ * on the screen for this specific breakpoint.
735+ */
736+ const activeHeight = modalHeight * breakpoint ;
737+
738+ /**
739+ * To find the Y coordinate, start at the bottom of the screen
740+ * and move up by the active height of the modal.
741+ *
742+ * A breakpoint of 1.0 means the active height is the full modal height
743+ * (fully open). A breakpoint of 0.0 means the active height is 0
744+ * (fully closed).
745+ *
746+ * Since screen Y coordinates get smaller as you go up, we subtract the
747+ * active height from the viewport bottom.
748+ */
749+ return viewportBottom - activeHeight ;
750+ } ;
751+
627752 const gesture = createGesture ( {
628753 el : wrapperEl ,
629754 gestureName : 'modalSheet' ,
0 commit comments