55 touchend ,
66 pointerdown ,
77 pointerenter ,
8- pointerleave
8+ pointerleave ,
9+ cancelPendingTouchDrag ,
10+ DDTouch
911} from '../src/dd-touch' ;
1012import { DDManager } from '../src/dd-manager' ;
1113import { Utils } from '../src/utils' ;
@@ -120,43 +122,37 @@ function createMockPointerEvent(type: string, pointerType: string, options: Part
120122 return mockEvent as PointerEvent ;
121123}
122124
125+ /** touch drags only start after the user pauses on the item - see #2781 */
126+ const TOUCH_DRAG_DELAY = 300 ;
127+ const pauseToDrag = ( ) => vi . advanceTimersByTime ( TOUCH_DRAG_DELAY ) ;
128+
123129describe ( 'dd-touch' , ( ) => {
124130 let mockUtils : any ;
125131 let mockDDManager : any ;
126132
133+ /** touchstart + wait out the pause, which is what actually starts the drag */
134+ function startTouchDrag ( touch : Touch ) : void {
135+ touchstart ( createMockTouchEvent ( 'touchstart' , [ touch ] ) ) ;
136+ pauseToDrag ( ) ;
137+ }
138+
127139 beforeEach ( ( ) => {
140+ vi . useFakeTimers ( ) ; // drags are now delayed, see #2781
128141 mockUtils = vi . mocked ( Utils ) ;
129142 mockDDManager = vi . mocked ( DDManager ) ;
130143
131144 // Reset mocks
132145 mockUtils . simulateMouseEvent . mockClear ( ) ;
133146 mockDDManager . dragElement = null ;
134147
135- // Mock window.clearTimeout and setTimeout
136- vi . spyOn ( window , 'clearTimeout' ) ;
137- vi . spyOn ( window , 'setTimeout' ) . mockImplementation ( ( callback : Function , delay : number ) => {
138- return setTimeout ( callback , delay ) as any ;
139- } ) ;
140-
141- // Reset DDTouch state by calling touchend to reset touchHandled flag
142- // This is a workaround since we can't access DDTouch directly
143- const resetTouch = {
144- pageX : 0 , pageY : 0 , clientX : 0 , clientY : 0 , screenX : 0 , screenY : 0 ,
145- identifier : 0 , target : document . createElement ( 'div' ) ,
146- radiusX : 0 , radiusY : 0 , rotationAngle : 0 , force : 0
147- } as Touch ;
148- const resetEvent = createMockTouchEvent ( 'touchend' , [ ] , { changedTouches : [ resetTouch ] } ) ;
149-
150- // Call touchstart then touchend to reset state
151- const startEvent = createMockTouchEvent ( 'touchstart' , [ resetTouch ] ) ;
152- touchstart ( startEvent ) ;
153- touchend ( resetEvent ) ;
154-
155- // Clear any calls made during reset
156- mockUtils . simulateMouseEvent . mockClear ( ) ;
148+ // reset leftover state from the previous gesture
149+ cancelPendingTouchDrag ( ) ;
150+ DDTouch . touchHandled = false ;
151+ delete DDTouch . pointerLeaveTimeout ;
157152 } ) ;
158153
159154 afterEach ( ( ) => {
155+ vi . useRealTimers ( ) ;
160156 vi . restoreAllMocks ( ) ;
161157 } ) ;
162158
@@ -192,18 +188,46 @@ describe('dd-touch', () => {
192188 } as Touch ;
193189 } ) ;
194190
195- it ( 'should simulate mousedown for single touch ' , ( ) => {
191+ it ( 'should simulate mousedown only after pausing on the item (#2781) ' , ( ) => {
196192 const mockTouchEvent = createMockTouchEvent ( 'touchstart' , [ mockTouch ] ) ;
197193
198194 touchstart ( mockTouchEvent ) ;
195+ expect ( mockUtils . simulateMouseEvent ) . not . toHaveBeenCalled ( ) ; // still could be a page scroll
199196
197+ pauseToDrag ( ) ;
200198 expect ( mockUtils . simulateMouseEvent ) . toHaveBeenCalledWith ( mockTouch , 'mousedown' ) ;
201199 } ) ;
202200
201+ it ( 'should let a quick swipe scroll the page instead of dragging (#2781)' , ( ) => {
202+ const target = document . createElement ( 'div' ) ;
203+ touchstart ( createMockTouchEvent ( 'touchstart' , [ mockTouch ] , { currentTarget : target } as never ) ) ;
204+
205+ // finger moves away before the delay elapses -> this is a scroll, not a drag
206+ const move : Event & { touches ?: unknown } = new Event ( 'touchmove' ) ;
207+ move . touches = [ { ...mockTouch , clientX : mockTouch . clientX + 50 } ] ;
208+ target . dispatchEvent ( move ) ;
209+ pauseToDrag ( ) ;
210+
211+ expect ( mockUtils . simulateMouseEvent ) . not . toHaveBeenCalled ( ) ;
212+ expect ( DDTouch . touchHandled ) . toBeFalsy ( ) ;
213+ } ) ;
214+
215+ it ( 'should not drag when releasing before the pause elapses (tap)' , ( ) => {
216+ const target = document . createElement ( 'div' ) ;
217+ touchstart ( createMockTouchEvent ( 'touchstart' , [ mockTouch ] , { currentTarget : target } as never ) ) ;
218+
219+ target . dispatchEvent ( new Event ( 'touchend' ) ) ;
220+ pauseToDrag ( ) ;
221+
222+ expect ( mockUtils . simulateMouseEvent ) . not . toHaveBeenCalled ( ) ;
223+ expect ( DDTouch . touchHandled ) . toBeFalsy ( ) ;
224+ } ) ;
225+
203226 it ( 'should prevent default on cancelable events' , ( ) => {
204227 const mockTouchEvent = createMockTouchEvent ( 'touchstart' , [ mockTouch ] , { cancelable : true } ) ;
205228
206229 touchstart ( mockTouchEvent ) ;
230+ pauseToDrag ( ) ;
207231
208232 expect ( mockTouchEvent . preventDefault ) . toHaveBeenCalled ( ) ;
209233 } ) ;
@@ -212,6 +236,7 @@ describe('dd-touch', () => {
212236 const mockTouchEvent = createMockTouchEvent ( 'touchstart' , [ mockTouch ] , { cancelable : false } ) ;
213237
214238 touchstart ( mockTouchEvent ) ;
239+ pauseToDrag ( ) ;
215240
216241 expect ( mockTouchEvent . preventDefault ) . not . toHaveBeenCalled ( ) ;
217242 } ) ;
@@ -221,6 +246,7 @@ describe('dd-touch', () => {
221246 const mockTouchEvent = createMockTouchEvent ( 'touchstart' , [ mockTouch , secondTouch ] ) ;
222247
223248 touchstart ( mockTouchEvent ) ;
249+ pauseToDrag ( ) ;
224250
225251 expect ( mockUtils . simulateMouseEvent ) . not . toHaveBeenCalled ( ) ;
226252 } ) ;
@@ -247,10 +273,7 @@ describe('dd-touch', () => {
247273 } ) ;
248274
249275 it ( 'should simulate mousemove for single touch when touch is handled' , ( ) => {
250- // First call touchstart to set DDTouch.touchHandled = true
251- const startEvent = createMockTouchEvent ( 'touchstart' , [ mockTouch ] ) ;
252- touchstart ( startEvent ) ;
253-
276+ startTouchDrag ( mockTouch ) ;
254277 mockUtils . simulateMouseEvent . mockClear ( ) ; // Clear previous calls
255278
256279 const mockTouchEvent = createMockTouchEvent ( 'touchmove' , [ mockTouch ] ) ;
@@ -269,10 +292,7 @@ describe('dd-touch', () => {
269292 } ) ;
270293
271294 it ( 'should ignore multi-touch events' , ( ) => {
272- // First call touchstart to set DDTouch.touchHandled = true
273- const startEvent = createMockTouchEvent ( 'touchstart' , [ mockTouch ] ) ;
274- touchstart ( startEvent ) ;
275-
295+ startTouchDrag ( mockTouch ) ;
276296 mockUtils . simulateMouseEvent . mockClear ( ) ; // Clear previous calls
277297
278298 const secondTouch = { ...mockTouch , identifier : 2 } ;
@@ -305,10 +325,7 @@ describe('dd-touch', () => {
305325 } ) ;
306326
307327 it ( 'should simulate mouseup when touch is handled' , ( ) => {
308- // First call touchstart to set DDTouch.touchHandled = true
309- const startEvent = createMockTouchEvent ( 'touchstart' , [ mockTouch ] ) ;
310- touchstart ( startEvent ) ;
311-
328+ startTouchDrag ( mockTouch ) ;
312329 mockUtils . simulateMouseEvent . mockClear ( ) ; // Clear previous calls
313330
314331 const mockTouchEvent = createMockTouchEvent ( 'touchend' , [ ] , { changedTouches : [ mockTouch ] } ) ;
@@ -318,10 +335,7 @@ describe('dd-touch', () => {
318335 } ) ;
319336
320337 it ( 'should simulate click when not dragging' , ( ) => {
321- // First call touchstart to set DDTouch.touchHandled = true
322- const startEvent = createMockTouchEvent ( 'touchstart' , [ mockTouch ] ) ;
323- touchstart ( startEvent ) ;
324-
338+ startTouchDrag ( mockTouch ) ;
325339 mockUtils . simulateMouseEvent . mockClear ( ) ; // Clear previous calls
326340 mockDDManager . dragElement = null ; // Not dragging
327341
@@ -333,10 +347,7 @@ describe('dd-touch', () => {
333347 } ) ;
334348
335349 it ( 'should not simulate click when dragging' , ( ) => {
336- // First call touchstart to set DDTouch.touchHandled = true
337- const startEvent = createMockTouchEvent ( 'touchstart' , [ mockTouch ] ) ;
338- touchstart ( startEvent ) ;
339-
350+ startTouchDrag ( mockTouch ) ;
340351 mockUtils . simulateMouseEvent . mockClear ( ) ; // Clear previous calls
341352 mockDDManager . dragElement = { } ; // Dragging
342353
@@ -355,29 +366,16 @@ describe('dd-touch', () => {
355366 expect ( mockUtils . simulateMouseEvent ) . not . toHaveBeenCalled ( ) ;
356367 } ) ;
357368
358- it ( 'should clear pointerLeaveTimeout when it exists ' , ( ) => {
359- // First set up a pointerleave timeout
369+ it ( 'should cancel the pending mouseleave when releasing over ourself ' , ( ) => {
370+ startTouchDrag ( mockTouch ) ;
360371 mockDDManager . dragElement = { } ;
361- const pointerEvent = createMockPointerEvent ( 'pointerleave' , 'touch' ) ;
362-
363- let timeoutId : number ;
364- vi . mocked ( window . setTimeout ) . mockImplementation ( ( callback : Function , delay : number ) => {
365- timeoutId = 123 ;
366- return timeoutId as any ;
367- } ) ;
368-
369- pointerleave ( pointerEvent ) ;
370-
371- // Now call touchstart and touchend to trigger the timeout clearing
372- const startEvent = createMockTouchEvent ( 'touchstart' , [ mockTouch ] ) ;
373- touchstart ( startEvent ) ;
374-
372+ pointerleave ( createMockPointerEvent ( 'pointerleave' , 'touch' ) ) ; // leave we get right before the release
375373 mockUtils . simulateMouseEvent . mockClear ( ) ;
376-
377- const mockTouchEvent = createMockTouchEvent ( 'touchend' , [ ] , { changedTouches : [ mockTouch ] } ) ;
378- touchend ( mockTouchEvent ) ;
379374
380- expect ( window . clearTimeout ) . toHaveBeenCalledWith ( 123 ) ;
375+ touchend ( createMockTouchEvent ( 'touchend' , [ ] , { changedTouches : [ mockTouch ] } ) ) ;
376+ vi . advanceTimersByTime ( 50 ) ;
377+
378+ expect ( mockUtils . simulateMouseEvent ) . not . toHaveBeenCalledWith ( expect . anything ( ) , 'mouseleave' ) ;
381379 } ) ;
382380 } ) ;
383381
@@ -476,8 +474,8 @@ describe('dd-touch', () => {
476474 const mockPointerEvent = createMockPointerEvent ( 'pointerleave' , 'touch' ) ;
477475
478476 pointerleave ( mockPointerEvent ) ;
477+ vi . advanceTimersByTime ( 50 ) ;
479478
480- expect ( window . setTimeout ) . not . toHaveBeenCalled ( ) ;
481479 expect ( mockUtils . simulateMouseEvent ) . not . toHaveBeenCalled ( ) ;
482480 } ) ;
483481
@@ -486,29 +484,19 @@ describe('dd-touch', () => {
486484 const mockPointerEvent = createMockPointerEvent ( 'pointerleave' , 'mouse' ) ;
487485
488486 pointerleave ( mockPointerEvent ) ;
487+ vi . advanceTimersByTime ( 50 ) ;
489488
490- expect ( window . setTimeout ) . not . toHaveBeenCalled ( ) ;
491489 expect ( mockUtils . simulateMouseEvent ) . not . toHaveBeenCalled ( ) ;
492490 } ) ;
493491
494492 it ( 'should delay mouseleave simulation for touch events when dragging' , ( ) => {
495493 mockDDManager . dragElement = { } ;
496494 const mockPointerEvent = createMockPointerEvent ( 'pointerleave' , 'touch' ) ;
497-
498- // Mock setTimeout to capture the callback
499- let timeoutCallback : Function ;
500- vi . mocked ( window . setTimeout ) . mockImplementation ( ( callback : Function , delay : number ) => {
501- timeoutCallback = callback ;
502- return 123 as any ;
503- } ) ;
504495
505496 pointerleave ( mockPointerEvent ) ;
497+ expect ( mockUtils . simulateMouseEvent ) . not . toHaveBeenCalled ( ) ; // delayed so a release on ourself can cancel it
506498
507- expect ( window . setTimeout ) . toHaveBeenCalledWith ( expect . any ( Function ) , 10 ) ;
508-
509- // Execute the timeout callback
510- timeoutCallback ! ( ) ;
511-
499+ vi . advanceTimersByTime ( 50 ) ;
512500 expect ( mockUtils . simulateMouseEvent ) . toHaveBeenCalledWith ( mockPointerEvent , 'mouseleave' ) ;
513501 } ) ;
514502 } ) ;
0 commit comments