11import React , { useEffect } from 'react'
2+ import Hammer from 'hammerjs'
23import { createPortal } from 'react-dom'
34import { isModifierPressed , isTyping , Keybind } from '../../keybinding'
45import { rootRoute } from '../../router'
@@ -15,16 +16,15 @@ class Modal extends React.Component {
1516 this . state = {
1617 viewport : DEFAULT_WIDTH ,
1718 dragOffset : 0 ,
18- isClosing : false
19+ isDragging : false
1920 }
2021 this . node = React . createRef ( )
22+ this . hammerInstance = null
2123 this . handleClickOutside = this . handleClickOutside . bind ( this )
2224 this . handleResize = this . handleResize . bind ( this )
23- this . handleTouchStart = this . handleTouchStart . bind ( this )
24- this . handleTouchMove = this . handleTouchMove . bind ( this )
25- this . handleTouchEnd = this . handleTouchEnd . bind ( this )
26- this . touchStartY = null
27- this . lastTouchY = null
25+ this . handlePanMove = this . handlePanMove . bind ( this )
26+ this . handlePanEnd = this . handlePanEnd . bind ( this )
27+ this . handlePanCancel = this . handlePanCancel . bind ( this )
2828 }
2929
3030 componentDidMount ( ) {
@@ -36,6 +36,7 @@ class Modal extends React.Component {
3636 }
3737
3838 componentWillUnmount ( ) {
39+ this . teardownHammer ( )
3940 document . body . style . overflow = null
4041 document . body . style . height = null
4142 document . removeEventListener ( 'mousedown' , this . handleClickOutside )
@@ -51,7 +52,68 @@ class Modal extends React.Component {
5152 }
5253
5354 handleResize ( ) {
54- this . setState ( { viewport : window . innerWidth } )
55+ const viewport = window . innerWidth
56+ this . setState ( { viewport } )
57+ this . updateSwipeListener ( viewport )
58+ }
59+
60+ updateSwipeListener ( viewport ) {
61+ if ( ! this . node . current ) return
62+
63+ if ( viewport < MD_WIDTH ) {
64+ if ( this . hammerInstance ) return
65+
66+ const hammer = new Hammer ( this . node . current )
67+ hammer . get ( 'pan' ) . set ( { direction : Hammer . DIRECTION_VERTICAL , threshold : 0 } )
68+ hammer . on ( 'panmove' , this . handlePanMove )
69+ hammer . on ( 'panend' , this . handlePanEnd )
70+ hammer . on ( 'pancancel' , this . handlePanCancel )
71+ this . hammerInstance = hammer
72+ } else {
73+ this . teardownHammer ( )
74+ }
75+ }
76+
77+ handlePanMove ( ev ) {
78+ if ( ev . direction === Hammer . DIRECTION_DOWN || ev . deltaY > 0 ) {
79+ this . setState ( { dragOffset : ev . deltaY , isDragging : true } )
80+ }
81+ }
82+
83+ handlePanEnd ( ev ) {
84+ const shouldClose = ev . deltaY > 80 || ev . velocityY > 0.35
85+ if ( shouldClose ) {
86+ this . props . onClose ( )
87+ return
88+ }
89+
90+ // Snap back
91+ this . setState ( { dragOffset : 0 , isDragging : false } )
92+ }
93+
94+ handlePanCancel ( ) {
95+ this . setState ( { dragOffset : 0 , isDragging : false } )
96+ }
97+
98+ getDragStyle ( ) {
99+ const { dragOffset, isDragging } = this . state
100+ const clamped = Math . max ( 0 , dragOffset )
101+ const opacity = Math . max ( 0 , Math . min ( 1 , 1 - clamped / 200 ) )
102+ return {
103+ transform : `translateY(${ clamped } px)` ,
104+ opacity,
105+ transition : isDragging ? 'none' : 'transform 150ms ease-out, opacity 150ms ease-out'
106+ }
107+ }
108+
109+ teardownHammer ( ) {
110+ if ( this . hammerInstance ) {
111+ this . hammerInstance . off ( 'panmove' , this . handlePanMove )
112+ this . hammerInstance . off ( 'panend' , this . handlePanEnd )
113+ this . hammerInstance . off ( 'pancancel' , this . handlePanCancel )
114+ this . hammerInstance . destroy ( )
115+ this . hammerInstance = null
116+ }
55117 }
56118
57119 /**
@@ -70,65 +132,9 @@ class Modal extends React.Component {
70132 } else {
71133 styleObject . maxWidth = '880px'
72134 }
73- styleObject . transform = `translateY(${ this . state . dragOffset } px)`
74- if ( this . state . isClosing ) {
75- styleObject . transition = 'transform 150ms ease-out'
76- } else {
77- styleObject . transition =
78- this . state . dragOffset > 0 ? 'none' : 'transform 150ms ease-out'
79- }
80135 return styleObject
81136 }
82137
83- handleTouchStart ( e ) {
84- if ( this . state . viewport >= MD_WIDTH ) return
85- this . setState ( { isClosing : false } )
86- const touch = e . touches [ 0 ]
87- this . touchStartY = touch . clientY
88- this . lastTouchY = touch . clientY
89- }
90-
91- handleTouchMove ( e ) {
92- if ( this . state . viewport >= MD_WIDTH ) return
93- if ( this . touchStartY === null ) return
94- const touch = e . touches [ 0 ]
95- const deltaY = touch . clientY - this . touchStartY
96- this . lastTouchY = touch . clientY
97- if ( deltaY <= 0 ) {
98- this . setState ( { dragOffset : 0 } )
99- return
100- }
101- e . preventDefault ( )
102- this . setState ( { dragOffset : deltaY } )
103- }
104-
105- handleTouchEnd ( ) {
106- if ( this . state . viewport >= MD_WIDTH ) return
107- if ( this . touchStartY === null || this . lastTouchY === null ) {
108- this . touchStartY = null
109- this . lastTouchY = null
110- return
111- }
112- const deltaY = this . lastTouchY - this . touchStartY
113- this . touchStartY = null
114- this . lastTouchY = null
115- if ( deltaY > 150 ) {
116- this . setState (
117- {
118- dragOffset : Math . max ( deltaY , window . innerHeight * 0.6 ) ,
119- isClosing : true
120- } ,
121- ( ) => {
122- setTimeout ( ( ) => {
123- this . props . onClose ( )
124- } , 150 )
125- }
126- )
127- return
128- }
129- this . setState ( { dragOffset : 0 , isClosing : false } )
130- }
131-
132138 render ( ) {
133139 return createPortal (
134140 < >
@@ -145,12 +151,9 @@ class Modal extends React.Component {
145151 < div
146152 ref = { this . node }
147153 className = "max-h-[calc(100dvh_-_var(--gap)*2)] min-h-[66vh] md:min-h-120 w-full flex flex-col bg-white p-3 md:px-6 md:py-4 overflow-hidden box-border transition-[height] duration-200 ease-in shadow-2xl rounded-t-lg md:rounded-lg dark:bg-gray-900 focus:outline-hidden"
148- style = { this . getStyle ( ) }
154+ style = { { ... this . getStyle ( ) , ... this . getDragStyle ( ) } }
149155 // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
150156 tabIndex = { 0 }
151- onTouchStart = { this . handleTouchStart }
152- onTouchMove = { this . handleTouchMove }
153- onTouchEnd = { this . handleTouchEnd }
154157 >
155158 < FocusOnMount focusableRef = { this . node } />
156159 { this . props . children }
0 commit comments