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; } }