Skip to content

Commit c5be8d3

Browse files
crisbetommalerba
authored andcommittedDec 4, 2018
fix(drag-drop): account for out of view container and stacking order (#14257)
Currently we use the `ClientRect` of each drop container to determine whether an item can be dropped into it. Since the `ClientRect` doesn't know whether the container is scrolled out of the view or is positioned under another element, users can end up accidentally moving items into containers that aren't visible. These changes rework the logic to look for which containers intersect with the user's pointer, and to filter based on whether the container is the top-most element at the pointer's position. Fixes #14231.
1 parent 9c65c87 commit c5be8d3

File tree

4 files changed

+95
-4
lines changed

4 files changed

+95
-4
lines changed
 

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

+43
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {moveItemInArray} from './drag-utils';
2727
import {CdkDropList} from './drop-list';
2828
import {CdkDragHandle} from './drag-handle';
2929
import {CdkDropListGroup} from './drop-list-group';
30+
import {extendStyles} from './drag-styling';
3031

3132
const ITEM_HEIGHT = 25;
3233
const ITEM_WIDTH = 75;
@@ -1057,6 +1058,8 @@ describe('CdkDrag', () => {
10571058
.toBe('ltr', 'Expected preview element to inherit the directionality.');
10581059
expect(previewRect.width).toBe(itemRect.width, 'Expected preview width to match element');
10591060
expect(previewRect.height).toBe(itemRect.height, 'Expected preview height to match element');
1061+
expect(preview.style.pointerEvents)
1062+
.toBe('none', 'Expected pointer events to be disabled on the preview');
10601063

10611064
dispatchMouseEvent(document, 'mouseup');
10621065
fixture.detectChanges();
@@ -2349,6 +2352,46 @@ describe('CdkDrag', () => {
23492352
expect(Array.from(component.group._items)).toEqual([component.listOne, component.listTwo]);
23502353
}));
23512354

2355+
it('should not be able to drop an element into a container that is under another element',
2356+
fakeAsync(() => {
2357+
const fixture = createComponent(ConnectedDropZones);
2358+
fixture.detectChanges();
2359+
2360+
const groups = fixture.componentInstance.groupedDragItems.slice();
2361+
const element = groups[0][1].element.nativeElement;
2362+
const dropInstances = fixture.componentInstance.dropInstances.toArray();
2363+
const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect();
2364+
const coverElement = document.createElement('div');
2365+
const targetGroupRect = dropInstances[1].element.nativeElement.getBoundingClientRect();
2366+
2367+
// Add an extra element that covers the target container.
2368+
fixture.nativeElement.appendChild(coverElement);
2369+
extendStyles(coverElement.style, {
2370+
position: 'fixed',
2371+
top: targetGroupRect.top + 'px',
2372+
left: targetGroupRect.left + 'px',
2373+
bottom: targetGroupRect.bottom + 'px',
2374+
right: targetGroupRect.right + 'px',
2375+
background: 'orange'
2376+
});
2377+
2378+
dragElementViaMouse(fixture, element, targetRect.left + 1, targetRect.top + 1);
2379+
flush();
2380+
fixture.detectChanges();
2381+
2382+
const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0];
2383+
2384+
expect(event).toBeTruthy();
2385+
expect(event).toEqual({
2386+
previousIndex: 1,
2387+
currentIndex: 1,
2388+
item: groups[0][1],
2389+
container: dropInstances[0],
2390+
previousContainer: dropInstances[0],
2391+
isPointerOverContainer: false
2392+
});
2393+
}));
2394+
23522395
});
23532396

23542397
});

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

+3
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,9 @@ export class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
672672
}
673673

674674
extendStyles(preview.style, {
675+
// It's important that we disable the pointer events on the preview, because
676+
// it can throw off the `document.elementFromPoint` calls in the `CdkDropList`.
677+
pointerEvents: 'none',
675678
position: 'fixed',
676679
top: '0',
677680
left: '0',

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

+48-3
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ import {
2121
Directive,
2222
ChangeDetectorRef,
2323
SkipSelf,
24+
Inject,
2425
} from '@angular/core';
2526
import {Directionality} from '@angular/cdk/bidi';
27+
import {DOCUMENT} from '@angular/common';
2628
import {CdkDrag} from './drag';
2729
import {DragDropRegistry} from './drag-drop-registry';
2830
import {CdkDragDrop, CdkDragEnter, CdkDragExit, CdkDragSortEvent} from './drag-events';
@@ -93,6 +95,8 @@ interface ListPositionCacheEntry {
9395
}
9496
})
9597
export class CdkDropList<T = any> implements OnInit, OnDestroy {
98+
private _document: Document | undefined;
99+
96100
/** Draggable items in the container. */
97101
@ContentChildren(forwardRef(() => CdkDrag)) _draggables: QueryList<CdkDrag>;
98102

@@ -160,7 +164,17 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
160164
private _dragDropRegistry: DragDropRegistry<CdkDrag, CdkDropList<T>>,
161165
private _changeDetectorRef: ChangeDetectorRef,
162166
@Optional() private _dir?: Directionality,
163-
@Optional() @SkipSelf() private _group?: CdkDropListGroup<CdkDropList>) {}
167+
@Optional() @SkipSelf() private _group?: CdkDropListGroup<CdkDropList>,
168+
// @breaking-change 8.0.0 `_document` parameter to be made required.
169+
@Optional() @Inject(DOCUMENT) _document?: any) {
170+
171+
// @breaking-change 8.0.0 Remove null checks once `_document` parameter is required.
172+
if (_document) {
173+
this._document = _document;
174+
} else if (typeof document !== 'undefined') {
175+
this._document = document;
176+
}
177+
}
164178

165179
ngOnInit() {
166180
this._dragDropRegistry.registerDropContainer(this);
@@ -384,8 +398,39 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
384398
* @param y Position of the item along the Y axis.
385399
*/
386400
_getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number): CdkDropList | null {
387-
const result = this._positionCache.siblings
388-
.find(sibling => isInsideClientRect(sibling.clientRect, x, y));
401+
const results = this._positionCache.siblings.filter(sibling => {
402+
return isInsideClientRect(sibling.clientRect, x, y);
403+
});
404+
405+
// No drop containers are intersecting with the pointer.
406+
if (!results.length) {
407+
return null;
408+
}
409+
410+
let result: ListPositionCacheEntry | undefined = results[0];
411+
412+
// @breaking-change 8.0.0 remove null check once the
413+
// `_document` is made into a required parameter.
414+
if (this._document) {
415+
const elementFromPoint = this._document.elementFromPoint(x, y);
416+
417+
// If there's no element at the pointer position, then
418+
// the client rect is probably scrolled out of the view.
419+
if (!elementFromPoint) {
420+
return null;
421+
}
422+
423+
// The `ClientRect`, that we're using to find the container over which the user is
424+
// hovering, doesn't give us any information on whether the element has been scrolled
425+
// out of the view or whether it's overlapping with other containers. This means that
426+
// we could end up transferring the item into a container that's invisible or is positioned
427+
// below another one. We use the result from `elementFromPoint` to get the top-most element
428+
// at the pointer position and to find whether it's one of the intersecting drop containers.
429+
result = results.find(sibling => {
430+
const element = sibling.drop.element.nativeElement;
431+
return element === elementFromPoint || element.contains(elementFromPoint);
432+
});
433+
}
389434

390435
return result && result.drop.enterPredicate(item, result.drop) ? result.drop : null;
391436
}

‎tools/public_api_guard/cdk/drag-drop.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export declare class CdkDropList<T = any> implements OnInit, OnDestroy {
120120
lockAxis: 'x' | 'y';
121121
orientation: 'horizontal' | 'vertical';
122122
sorted: EventEmitter<CdkDragSortEvent<T>>;
123-
constructor(element: ElementRef<HTMLElement>, _dragDropRegistry: DragDropRegistry<CdkDrag, CdkDropList<T>>, _changeDetectorRef: ChangeDetectorRef, _dir?: Directionality | undefined, _group?: CdkDropListGroup<CdkDropList<any>> | undefined);
123+
constructor(element: ElementRef<HTMLElement>, _dragDropRegistry: DragDropRegistry<CdkDrag, CdkDropList<T>>, _changeDetectorRef: ChangeDetectorRef, _dir?: Directionality | undefined, _group?: CdkDropListGroup<CdkDropList<any>> | undefined, _document?: any);
124124
_getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number): CdkDropList | null;
125125
_isOverContainer(x: number, y: number): boolean;
126126
_sortItem(item: CdkDrag, pointerX: number, pointerY: number, pointerDelta: {

0 commit comments

Comments
 (0)
Please sign in to comment.