diff --git a/projects/ngx-datatable/src/lib/directives/datatable-draggable.directive.ts b/projects/ngx-datatable/src/lib/directives/datatable-draggable.directive.ts index e39e53c61..9f397a904 100644 --- a/projects/ngx-datatable/src/lib/directives/datatable-draggable.directive.ts +++ b/projects/ngx-datatable/src/lib/directives/datatable-draggable.directive.ts @@ -56,9 +56,11 @@ export class DatatableDraggableDirective implements OnDestroy { if (this.enabled()) { this.element.addEventListener('mousedown', this.mousedown); this.element.addEventListener('touchstart', this.touchstart); + this.element.addEventListener('contextmenu', this.contextmenu); this.removePointerListeners = () => { this.element.removeEventListener('mousedown', this.mousedown); this.element.removeEventListener('touchstart', this.touchstart); + this.element.removeEventListener('contextmenu', this.contextmenu); }; } else { this.removePointerListeners?.(); @@ -83,30 +85,44 @@ export class DatatableDraggableDirective implements OnDestroy { this.delay(this.dragStartDelay()).then(() => { this.document.addEventListener('mousemove', this.mousemove); this.starting(event.clientX, event.clientY); + this.setDragging(true); }); }; private mousemove = (event: MouseEvent): void => this.moving(event.clientX, event.clientY); + // Prevent context menu on long-press drag. Since we don't call preventDefault() on + // touchstart to allow click events (sorting), the browser would show a context menu + // after a long press. We prevent this when dragging is active. + private contextmenu = (event: MouseEvent): void => { + if (this.isDragging()) { + event.preventDefault(); + } + }; + protected readonly touchstart = (event: TouchEvent): void => { if (!this.enabled()) { return; } event.stopPropagation(); - event.preventDefault(); const touch = event.touches.item(0)!; this.touchId = touch.identifier; this.document.addEventListener('touchend', this.ending); this.delay(this.dragStartDelay()).then(() => { - this.document.addEventListener('touchmove', this.touchmove); - this.starting(touch.clientX, touch.clientY); + if (this.touchId === touch.identifier) { + this.document.addEventListener('touchmove', this.touchmove, { passive: false }); + this.starting(touch.clientX, touch.clientY); + this.setDragging(true); + } }); }; private touchmove = (event: TouchEvent): void => { - const touchMove = this.findTouch(event)!; + const touchMove = this.findTouch(event); if (touchMove) { + // Prevent scrolling and other default touch behaviors during drag + event.preventDefault(); this.moving(touchMove.clientX, touchMove.clientY); } }; @@ -139,10 +155,18 @@ export class DatatableDraggableDirective implements OnDestroy { // This function is also called if the long press was aborted before the delay. // In that case, we don't want to emit dragEnd. if (dragged) { + this.setDragging(false); this.dragEnd.emit(); } }; + private setDragging(dragging: boolean): void { + const model = this.dragModel(); + if (model) { + model.dragging = dragging; + } + } + private findTouch(event: TouchEvent): Touch | undefined { return Array.from(event.touches).find(touch => touch.identifier === this.touchId); } diff --git a/projects/ngx-datatable/src/lib/directives/draggable.directive.spec.ts b/projects/ngx-datatable/src/lib/directives/draggable.directive.spec.ts index e65984523..47b69fcb7 100644 --- a/projects/ngx-datatable/src/lib/directives/draggable.directive.spec.ts +++ b/projects/ngx-datatable/src/lib/directives/draggable.directive.spec.ts @@ -161,13 +161,16 @@ describe('DraggableDirective', () => { await fixture.whenStable(); }); + afterEach(() => { + vi.useRealTimers(); + }); + it('should start dragging after the specified delay', async () => { vi.useFakeTimers(); await harness.touchStart(0); vi.advanceTimersByTime(100); await fixture.whenStable(); expect(dragStartSpy).toHaveBeenCalled(); - vi.useRealTimers(); }); it('should skip dragging if not pressed long enough', async () => { @@ -177,7 +180,6 @@ describe('DraggableDirective', () => { await harness.mouseUp(); expect(dragStartSpy).not.toHaveBeenCalled(); expect(dragEndSpy).not.toHaveBeenCalled(); - vi.useRealTimers(); }); it('should not start dragging if waiting for the delay', async () => { @@ -186,7 +188,14 @@ describe('DraggableDirective', () => { vi.advanceTimersByTime(50); await harness.mouseMove(100); expect(dragMoveSpy).not.toHaveBeenCalled(); - vi.useRealTimers(); + }); + + it('should not start dragging after short touch before delay', async () => { + vi.useFakeTimers(); + await harness.touchStart(0); + vi.advanceTimersByTime(50); + await harness.touchEnd(); + expect(dragStartSpy).not.toHaveBeenCalled(); }); }); });