Skip to content

Commit f5d5a53

Browse files
committed
fix: add touch cancel cleanup
1 parent 211cc62 commit f5d5a53

2 files changed

Lines changed: 72 additions & 5 deletions

File tree

packages/table-core/src/features/column-resizing/columnResizingFeature.utils.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -263,17 +263,29 @@ export function header_getResizeHandler<
263263
return false
264264
},
265265
upHandler: (e: TouchEvent) => {
266-
contextDocument?.removeEventListener(
267-
'touchmove',
268-
touchEvents.moveHandler,
269-
)
270-
contextDocument?.removeEventListener('touchend', touchEvents.upHandler)
266+
removeTouchEvents()
271267
if (e.cancelable) {
272268
e.preventDefault()
273269
e.stopPropagation()
274270
}
275271
onEnd(e.touches[0]?.clientX)
276272
},
273+
// the browser fires touchcancel instead of touchend when it takes over
274+
// the gesture (system gesture, scroll takeover, tab switch); without
275+
// this the non-passive touchmove listener stays on the document forever
276+
cancelHandler: () => {
277+
removeTouchEvents()
278+
onEnd()
279+
},
280+
}
281+
282+
const removeTouchEvents = () => {
283+
contextDocument?.removeEventListener('touchmove', touchEvents.moveHandler)
284+
contextDocument?.removeEventListener('touchend', touchEvents.upHandler)
285+
contextDocument?.removeEventListener(
286+
'touchcancel',
287+
touchEvents.cancelHandler,
288+
)
277289
}
278290

279291
const passiveIfSupported = passiveEventSupported()
@@ -291,6 +303,11 @@ export function header_getResizeHandler<
291303
touchEvents.upHandler,
292304
passiveIfSupported,
293305
)
306+
contextDocument?.addEventListener(
307+
'touchcancel',
308+
touchEvents.cancelHandler,
309+
passiveIfSupported,
310+
)
294311
} else {
295312
contextDocument?.addEventListener(
296313
'mousemove',

packages/table-core/tests/unit/features/column-resizing/columnResizingFeature.utils.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,56 @@ describe('header_getResizeHandler', () => {
582582

583583
removeEventListenerSpy.mockRestore()
584584
})
585+
586+
it('should cleanup event listeners and reset state on touchcancel', () => {
587+
const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener')
588+
const table = makeTable(1, {
589+
columnResizeMode: 'onChange',
590+
})
591+
const sizingUpdates: Array<Record<string, number>> = []
592+
table.options.onColumnSizingChange = (updater) => {
593+
sizingUpdates.push(
594+
typeof updater === 'function'
595+
? updater(table.atoms.columnSizing.get())
596+
: updater,
597+
)
598+
}
599+
600+
const header = createTestResizeHeader(table)
601+
const handler = header_getResizeHandler(header as any, document)
602+
handler({ type: 'touchstart', touches: [{ clientX: 100 }] })
603+
604+
document.dispatchEvent(
605+
Object.assign(new Event('touchmove'), { touches: [{ clientX: 150 }] }),
606+
)
607+
expect(sizingUpdates).toHaveLength(1)
608+
609+
removeEventListenerSpy.mockClear()
610+
document.dispatchEvent(new Event('touchcancel'))
611+
612+
expect(removeEventListenerSpy).toHaveBeenCalledWith(
613+
'touchmove',
614+
expect.any(Function),
615+
)
616+
expect(removeEventListenerSpy).toHaveBeenCalledWith(
617+
'touchend',
618+
expect.any(Function),
619+
)
620+
expect(removeEventListenerSpy).toHaveBeenCalledWith(
621+
'touchcancel',
622+
expect.any(Function),
623+
)
624+
expect(table.atoms.columnResizing.get().isResizingColumn).toBe(false)
625+
626+
// cancel commits at the last observed position, then moves are ignored
627+
const updatesAfterCancel = sizingUpdates.length
628+
document.dispatchEvent(
629+
Object.assign(new Event('touchmove'), { touches: [{ clientX: 300 }] }),
630+
)
631+
expect(sizingUpdates).toHaveLength(updatesAfterCancel)
632+
633+
removeEventListenerSpy.mockRestore()
634+
})
585635
})
586636

587637
describe('passiveEventSupported', () => {

0 commit comments

Comments
 (0)