Skip to content

Commit 0a6e4d6

Browse files
authored
fix(drag-drop): sometimes incorrectly swapping items at the ends of the list (#19633)
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.
1 parent da9c707 commit 0a6e4d6

File tree

2 files changed

+57
-9
lines changed

2 files changed

+57
-9
lines changed

src/cdk/drag-drop/directives/drag.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2878,6 +2878,48 @@ describe('CdkDrag', () => {
28782878
flush();
28792879
}));
28802880

2881+
it('it should allow item swaps in the same drag direction, if the pointer did not ' +
2882+
'overlap with the sibling item after the previous swap', fakeAsync(() => {
2883+
const fixture = createComponent(DraggableInDropZone);
2884+
fixture.detectChanges();
2885+
2886+
const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement);
2887+
const draggedItem = items[0];
2888+
const target = items[items.length - 1];
2889+
const itemRect = draggedItem.getBoundingClientRect();
2890+
2891+
startDraggingViaMouse(fixture, draggedItem, itemRect.left, itemRect.top);
2892+
2893+
const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement;
2894+
2895+
expect(getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()))
2896+
.toEqual(['Zero', 'One', 'Two', 'Three']);
2897+
2898+
let targetRect = target.getBoundingClientRect();
2899+
2900+
// Trigger a mouse move coming from the bottom so that the list thinks that we're
2901+
// sorting upwards. This usually how a user would behave with a mouse pointer.
2902+
dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.bottom + 50);
2903+
fixture.detectChanges();
2904+
dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.bottom - 1);
2905+
fixture.detectChanges();
2906+
2907+
expect(getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()))
2908+
.toEqual(['One', 'Two', 'Three', 'Zero']);
2909+
2910+
// Refresh the rect since the element position has changed.
2911+
targetRect = target.getBoundingClientRect();
2912+
dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.bottom - 1);
2913+
fixture.detectChanges();
2914+
2915+
expect(getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()))
2916+
.toEqual(['One', 'Two', 'Zero', 'Three']);
2917+
2918+
dispatchMouseEvent(document, 'mouseup');
2919+
fixture.detectChanges();
2920+
flush();
2921+
}));
2922+
28812923
it('should clean up the preview element if the item is destroyed mid-drag', fakeAsync(() => {
28822924
const fixture = createComponent(DraggableInDropZone);
28832925
fixture.detectChanges();

src/cdk/drag-drop/drop-list-ref.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,11 @@ export class DropListRef<T = any> {
153153
private _activeDraggables: DragRef[];
154154

155155
/**
156-
* Keeps track of the item that was last swapped with the dragged item, as
157-
* well as what direction the pointer was moving in when the swap occured.
156+
* Keeps track of the item that was last swapped with the dragged item, as well as what direction
157+
* the pointer was moving in when the swap occured and whether the user's pointer continued to
158+
* overlap with the swapped item after the swapping occurred.
158159
*/
159-
private _previousSwap = {drag: null as DragRef | null, delta: 0};
160+
private _previousSwap = {drag: null as DragRef | null, delta: 0, overlaps: false};
160161

161162
/** Draggable items in the container. */
162163
private _draggables: ReadonlyArray<DragRef>;
@@ -485,9 +486,6 @@ export class DropListRef<T = any> {
485486
const newPosition = siblingAtNewPosition.clientRect;
486487
const delta = currentIndex > newIndex ? 1 : -1;
487488

488-
this._previousSwap.drag = siblingAtNewPosition.drag;
489-
this._previousSwap.delta = isHorizontal ? pointerDelta.x : pointerDelta.y;
490-
491489
// How many pixels the item's placeholder should be offset.
492490
const itemOffset = this._getItemOffsetPx(currentPosition, newPosition, delta);
493491

@@ -536,6 +534,11 @@ export class DropListRef<T = any> {
536534
adjustClientRect(sibling.clientRect, offset, 0);
537535
}
538536
});
537+
538+
// Note that it's important that we do this after the client rects have been adjusted.
539+
this._previousSwap.overlaps = isInsideClientRect(newPosition, pointerX, pointerY);
540+
this._previousSwap.drag = siblingAtNewPosition.drag;
541+
this._previousSwap.delta = isHorizontal ? pointerDelta.x : pointerDelta.y;
539542
}
540543

541544
/**
@@ -644,6 +647,7 @@ export class DropListRef<T = any> {
644647
this._itemPositions = [];
645648
this._previousSwap.drag = null;
646649
this._previousSwap.delta = 0;
650+
this._previousSwap.overlaps = false;
647651
this._stopScrolling();
648652
this._viewportScrollSubscription.unsubscribe();
649653
this._parentPositions.clear();
@@ -748,9 +752,11 @@ export class DropListRef<T = any> {
748752
if (delta) {
749753
const direction = isHorizontal ? delta.x : delta.y;
750754

751-
// If the user is still hovering over the same item as last time, and they didn't change
752-
// the direction in which they're dragging, we don't consider it a direction swap.
753-
if (drag === this._previousSwap.drag && direction === this._previousSwap.delta) {
755+
// If the user is still hovering over the same item as last time, their cursor hasn't left
756+
// the item after we made the swap, and they didn't change the direction in which they're
757+
// dragging, we don't consider it a direction swap.
758+
if (drag === this._previousSwap.drag && this._previousSwap.overlaps &&
759+
direction === this._previousSwap.delta) {
754760
return false;
755761
}
756762
}

0 commit comments

Comments
 (0)