From 1456074baae6ba1f2ff82f3bf27b1fd9bb1b9017 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 16 Dec 2024 17:57:56 +0200 Subject: [PATCH] fix(cdk/drag-drop): stop dragging on touchcancel (#30184) In some cases we might not get a `touchend`, because the sequence was interrupted. These changes add a fallback. (cherry picked from commit 454d9f9c0937e9e70d018be13a7f452bec776620) --- .../directives/standalone-drag.spec.ts | 16 ++++++++++++++++ src/cdk/drag-drop/drag-drop-registry.spec.ts | 13 +++++++++++++ src/cdk/drag-drop/drag-drop-registry.ts | 19 +++++++++++++------ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/cdk/drag-drop/directives/standalone-drag.spec.ts b/src/cdk/drag-drop/directives/standalone-drag.spec.ts index c4449abe204e..d6b6be72a72b 100644 --- a/src/cdk/drag-drop/directives/standalone-drag.spec.ts +++ b/src/cdk/drag-drop/directives/standalone-drag.spec.ts @@ -364,6 +364,22 @@ describe('Standalone CdkDrag', () => { expect(dragElement.style.transform).toBeFalsy(); })); + + it('should stop dragging on touchcancel', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const x = 50; + const y = 100; + + expect(dragElement.style.transform).toBeFalsy(); + startDraggingViaTouch(fixture, dragElement); + continueDraggingViaTouch(fixture, x, y); + dispatchTouchEvent(document, 'touchcancel', x, y); + fixture.detectChanges(); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + expect(fixture.componentInstance.endedSpy).toHaveBeenCalled(); + })); }); describe('mouse dragging when initial transform is none', () => { diff --git a/src/cdk/drag-drop/drag-drop-registry.spec.ts b/src/cdk/drag-drop/drag-drop-registry.spec.ts index 935b94ba5908..5d12b15e5c42 100644 --- a/src/cdk/drag-drop/drag-drop-registry.spec.ts +++ b/src/cdk/drag-drop/drag-drop-registry.spec.ts @@ -243,6 +243,19 @@ describe('DragDropRegistry', () => { subscription.unsubscribe(); }); + it('should dispatch `touchcancel` events if the drag was interrupted', () => { + const spy = jasmine.createSpy('pointerUp spy'); + const subscription = registry.pointerUp.subscribe(spy); + const item = new DragItem() as unknown as DragRef; + + registry.startDragging(item, createTouchEvent('touchstart') as TouchEvent); + const event = dispatchTouchEvent(document, 'touchcancel'); + + expect(spy).toHaveBeenCalledWith(event); + + subscription.unsubscribe(); + }); + class DragItem { isDragging() { return this.shouldBeDragging; diff --git a/src/cdk/drag-drop/drag-drop-registry.ts b/src/cdk/drag-drop/drag-drop-registry.ts index a8e2bd079fc7..5667c6f78a47 100644 --- a/src/cdk/drag-drop/drag-drop-registry.ts +++ b/src/cdk/drag-drop/drag-drop-registry.ts @@ -170,16 +170,23 @@ export class DragDropRegistry<_ = unknown, __ = unknown> implements OnDestroy { this._activeDragInstances.update(instances => [...instances, drag]); if (this._activeDragInstances().length === 1) { - const isTouchEvent = event.type.startsWith('touch'); - // We explicitly bind __active__ listeners here, because newer browsers will default to // passive ones for `mousemove` and `touchmove`. The events need to be active, because we // use `preventDefault` to prevent the page from scrolling while the user is dragging. + const isTouchEvent = event.type.startsWith('touch'); + const endEventHandler = { + handler: (e: Event) => this.pointerUp.next(e as TouchEvent | MouseEvent), + options: true, + }; + + if (isTouchEvent) { + this._globalListeners.set('touchend', endEventHandler); + this._globalListeners.set('touchcancel', endEventHandler); + } else { + this._globalListeners.set('mouseup', endEventHandler); + } + this._globalListeners - .set(isTouchEvent ? 'touchend' : 'mouseup', { - handler: (e: Event) => this.pointerUp.next(e as TouchEvent | MouseEvent), - options: true, - }) .set('scroll', { handler: (e: Event) => this.scroll.next(e), // Use capturing so that we pick up scroll changes in any scrollable nodes that aren't