Skip to content

Commit 0ac41a0

Browse files
crisbetojelbourn
authored andcommitted
fix(drag-drop): unable to return item to initial container within same drag sequence, if not connected to current drag container (#13247)
Handles the case where the consumer has two drop containers where only one is connected to the other. Currently once the user drags an element outside the first one, they won't be able to return it. With these changes we allow the element to be returned, as long as it hasn't been dropped into the new container. Fixes #13246.
1 parent 40fb5cb commit 0ac41a0

File tree

4 files changed

+81
-7
lines changed

4 files changed

+81
-7
lines changed

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1507,6 +1507,49 @@ describe('CdkDrag', () => {
15071507
'Expected CdkDrag to be returned to first container in memory');
15081508
}));
15091509

1510+
it('should be able to return an element to its initial container in the same sequence, ' +
1511+
'even if it is not connected to the current container', fakeAsync(() => {
1512+
const fixture = createComponent(ConnectedDropZones);
1513+
fixture.detectChanges();
1514+
1515+
const groups = fixture.componentInstance.groupedDragItems;
1516+
const dropInstances = fixture.componentInstance.dropInstances.toArray();
1517+
const dropZones = dropInstances.map(d => d.element.nativeElement);
1518+
const item = groups[0][1];
1519+
const initialRect = item.element.nativeElement.getBoundingClientRect();
1520+
const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect();
1521+
1522+
// Change the `connectedTo` so the containers are only connected one-way.
1523+
dropInstances[0].connectedTo = dropInstances[1];
1524+
dropInstances[1].connectedTo = [];
1525+
1526+
dispatchMouseEvent(item.element.nativeElement, 'mousedown');
1527+
fixture.detectChanges();
1528+
1529+
const placeholder = dropZones[0].querySelector('.cdk-drag-placeholder')!;
1530+
1531+
expect(placeholder).toBeTruthy();
1532+
expect(dropZones[0].contains(placeholder))
1533+
.toBe(true, 'Expected placeholder to be inside the first container.');
1534+
1535+
dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1);
1536+
fixture.detectChanges();
1537+
1538+
expect(dropZones[1].contains(placeholder))
1539+
.toBe(true, 'Expected placeholder to be inside second container.');
1540+
1541+
dispatchMouseEvent(document, 'mousemove', initialRect.left + 1, initialRect.top + 1);
1542+
fixture.detectChanges();
1543+
1544+
expect(dropZones[0].contains(placeholder))
1545+
.toBe(true, 'Expected placeholder to be back inside first container.');
1546+
1547+
dispatchMouseEvent(document, 'mouseup');
1548+
fixture.detectChanges();
1549+
1550+
expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled();
1551+
}));
1552+
15101553
});
15111554

15121555
});

src/cdk/drag-drop/drag.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -430,16 +430,25 @@ export class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
430430
*/
431431
private _updateActiveDropContainer({x, y}) {
432432
// Drop container that draggable has been moved into.
433-
const newContainer = this.dropContainer._getSiblingContainerFromPosition(this, x, y);
433+
let newContainer = this.dropContainer._getSiblingContainerFromPosition(this, x, y);
434+
435+
// If we couldn't find a new container to move the item into, and the item has left it's
436+
// initial container, check whether the it's allowed to return into its original container.
437+
// This handles the case where two containers are connected one way and the user tries to
438+
// undo dragging an item into a new container.
439+
if (!newContainer && this.dropContainer !== this._initialContainer &&
440+
this._initialContainer._canReturnItem(this, x, y)) {
441+
newContainer = this._initialContainer;
442+
}
434443

435444
if (newContainer) {
436445
this._ngZone.run(() => {
437446
// Notify the old container that the item has left.
438447
this.exited.emit({item: this, container: this.dropContainer});
439448
this.dropContainer.exit(this);
440449
// Notify the new container that the item has entered.
441-
this.entered.emit({item: this, container: newContainer});
442-
this.dropContainer = newContainer;
450+
this.entered.emit({item: this, container: newContainer!});
451+
this.dropContainer = newContainer!;
443452
this.dropContainer.enter(this, x, y);
444453
});
445454
}

src/cdk/drag-drop/drop-container.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export interface CdkDropContainer<T = any> {
5959
_sortItem(item: CdkDrag, pointerX: number, pointerY: number, delta: {x: number, y: number}): void;
6060
_draggables: QueryList<CdkDrag>;
6161
_getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number): CdkDropContainer | null;
62+
_canReturnItem(item: CdkDrag, x: number, y: number): boolean;
6263
}
6364

6465
/**

src/cdk/drag-drop/drop.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -304,14 +304,23 @@ export class CdkDrop<T = any> implements OnInit, OnDestroy {
304304
* @param y Position of the item along the Y axis.
305305
*/
306306
_getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number): CdkDrop | null {
307-
const result = this._positionCache.siblings.find(({clientRect}) => {
308-
const {top, bottom, left, right} = clientRect;
309-
return y >= top && y <= bottom && x >= left && x <= right;
310-
});
307+
const result = this._positionCache.siblings
308+
.find(sibling => isInsideClientRect(sibling.clientRect, x, y));
311309

312310
return result && result.drop.enterPredicate(item, this) ? result.drop : null;
313311
}
314312

313+
/**
314+
* Checks whether an item that started in this container can be returned to it,
315+
* after it was moved out into another container.
316+
* @param item Item that is being checked.
317+
* @param x Position of the item along the X axis.
318+
* @param y Position of the item along the Y axis.
319+
*/
320+
_canReturnItem(item: CdkDrag, x: number, y: number): boolean {
321+
return isInsideClientRect(this._positionCache.self, x, y) && this.enterPredicate(item, this);
322+
}
323+
315324
/** Refreshes the position cache of the items and sibling containers. */
316325
private _cachePositions() {
317326
const isHorizontal = this.orientation === 'horizontal';
@@ -451,3 +460,15 @@ function findIndex<T>(array: T[],
451460

452461
return -1;
453462
}
463+
464+
465+
/**
466+
* Checks whether some coordinates are within a `ClientRect`.
467+
* @param clientRect ClientRect that is being checked.
468+
* @param x Coordinates along the X axis.
469+
* @param y Coordinates along the Y axis.
470+
*/
471+
function isInsideClientRect(clientRect: ClientRect, x: number, y: number) {
472+
const {top, bottom, left, right} = clientRect;
473+
return y >= top && y <= bottom && x >= left && x <= right;
474+
}

0 commit comments

Comments
 (0)