From 0d7446c8d0bb35c2d71cd85d0e30eb9df044823b Mon Sep 17 00:00:00 2001 From: crisbeto Date: Sat, 13 Jun 2020 11:19:14 +0200 Subject: [PATCH] fix(drag-drop): sometimes incorrectly swapping items at the ends of the list The drop list has some logic that doesn't allow a swap to occur if the items being swapped are the same as the ones in the previous swap and the direction of the user's pointer hasn't changed. This is set up so that we don't trigger a swap for every pixel, if the user's pointer lands on a different item after the previous swap is done. The above-mentioned logic can be problematic if the user takes an item from the middle, drags it out of the list and then inserts it at the end while dragging upwards. In this case the list won't allow the swap, because the item is the same as the one in the previous swap and the pointer's direction hasn't changed. These changes resolve the issue by preventing the swap only if the user's pointer landed on the same item after the previous swap was finished. Fixes #19249. --- src/cdk/drag-drop/directives/drag.spec.ts | 42 +++++++++++++++++++++++ src/cdk/drag-drop/drop-list-ref.ts | 24 ++++++++----- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index 97077253ebe3..ee571769b4e1 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -2817,6 +2817,48 @@ describe('CdkDrag', () => { flush(); })); + it('it should allow item swaps in the same drag direction, if the pointer did not ' + + 'overlap with the sibling item after the previous swap', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZone); + fixture.detectChanges(); + + const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); + const draggedItem = items[0]; + const target = items[items.length - 1]; + const itemRect = draggedItem.getBoundingClientRect(); + + startDraggingViaMouse(fixture, draggedItem, itemRect.left, itemRect.top); + + const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; + + expect(getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim())) + .toEqual(['Zero', 'One', 'Two', 'Three']); + + let targetRect = target.getBoundingClientRect(); + + // Trigger a mouse move coming from the bottom so that the list thinks that we're + // sorting upwards. This usually how a user would behave with a mouse pointer. + dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.bottom + 50); + fixture.detectChanges(); + dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.bottom - 1); + fixture.detectChanges(); + + expect(getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim())) + .toEqual(['One', 'Two', 'Three', 'Zero']); + + // Refresh the rect since the element position has changed. + targetRect = target.getBoundingClientRect(); + dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.bottom - 1); + fixture.detectChanges(); + + expect(getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim())) + .toEqual(['One', 'Two', 'Zero', 'Three']); + + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + flush(); + })); + it('should clean up the preview element if the item is destroyed mid-drag', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); fixture.detectChanges(); diff --git a/src/cdk/drag-drop/drop-list-ref.ts b/src/cdk/drag-drop/drop-list-ref.ts index a33302fa9cdf..039ad371b8f9 100644 --- a/src/cdk/drag-drop/drop-list-ref.ts +++ b/src/cdk/drag-drop/drop-list-ref.ts @@ -153,10 +153,11 @@ export class DropListRef { private _activeDraggables: DragRef[]; /** - * Keeps track of the item that was last swapped with the dragged item, as - * well as what direction the pointer was moving in when the swap occured. + * Keeps track of the item that was last swapped with the dragged item, as well as what direction + * the pointer was moving in when the swap occured and whether the user's pointer continued to + * overlap with the swapped item after the swapping occurred. */ - private _previousSwap = {drag: null as DragRef | null, delta: 0}; + private _previousSwap = {drag: null as DragRef | null, delta: 0, overlaps: false}; /** Draggable items in the container. */ private _draggables: ReadonlyArray; @@ -485,9 +486,6 @@ export class DropListRef { const newPosition = siblingAtNewPosition.clientRect; const delta = currentIndex > newIndex ? 1 : -1; - this._previousSwap.drag = siblingAtNewPosition.drag; - this._previousSwap.delta = isHorizontal ? pointerDelta.x : pointerDelta.y; - // How many pixels the item's placeholder should be offset. const itemOffset = this._getItemOffsetPx(currentPosition, newPosition, delta); @@ -536,6 +534,11 @@ export class DropListRef { adjustClientRect(sibling.clientRect, offset, 0); } }); + + // Note that it's important that we do this after the client rects have been adjusted. + this._previousSwap.overlaps = isInsideClientRect(newPosition, pointerX, pointerY); + this._previousSwap.drag = siblingAtNewPosition.drag; + this._previousSwap.delta = isHorizontal ? pointerDelta.x : pointerDelta.y; } /** @@ -644,6 +647,7 @@ export class DropListRef { this._itemPositions = []; this._previousSwap.drag = null; this._previousSwap.delta = 0; + this._previousSwap.overlaps = false; this._stopScrolling(); this._viewportScrollSubscription.unsubscribe(); this._parentPositions.clear(); @@ -748,9 +752,11 @@ export class DropListRef { if (delta) { const direction = isHorizontal ? delta.x : delta.y; - // If the user is still hovering over the same item as last time, and they didn't change - // the direction in which they're dragging, we don't consider it a direction swap. - if (drag === this._previousSwap.drag && direction === this._previousSwap.delta) { + // If the user is still hovering over the same item as last time, their cursor hasn't left + // the item after we made the swap, and they didn't change the direction in which they're + // dragging, we don't consider it a direction swap. + if (drag === this._previousSwap.drag && this._previousSwap.overlaps && + direction === this._previousSwap.delta) { return false; } }