Skip to content

Commit

Permalink
fix(cdk/drag-drop): stop dragging on touchcancel (#30184)
Browse files Browse the repository at this point in the history
In some cases we might not get a `touchend`, because the sequence was interrupted. These changes add a fallback.
  • Loading branch information
crisbeto authored Dec 16, 2024
1 parent 04e5e0f commit 454d9f9
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 6 deletions.
16 changes: 16 additions & 0 deletions src/cdk/drag-drop/directives/standalone-drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
13 changes: 13 additions & 0 deletions src/cdk/drag-drop/drag-drop-registry.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
19 changes: 13 additions & 6 deletions src/cdk/drag-drop/drag-drop-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 454d9f9

Please sign in to comment.