Skip to content

fix(drag-drop): account for out of view container and stacking order #14257

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/cdk/drag-drop/drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {moveItemInArray} from './drag-utils';
import {CdkDropList} from './drop-list';
import {CdkDragHandle} from './drag-handle';
import {CdkDropListGroup} from './drop-list-group';
import {extendStyles} from './drag-styling';

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

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

it('should not be able to drop an element into a container that is under another element',
fakeAsync(() => {
const fixture = createComponent(ConnectedDropZones);
fixture.detectChanges();

const groups = fixture.componentInstance.groupedDragItems.slice();
const element = groups[0][1].element.nativeElement;
const dropInstances = fixture.componentInstance.dropInstances.toArray();
const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect();
const coverElement = document.createElement('div');
const targetGroupRect = dropInstances[1].element.nativeElement.getBoundingClientRect();

// Add an extra element that covers the target container.
fixture.nativeElement.appendChild(coverElement);
extendStyles(coverElement.style, {
position: 'fixed',
top: targetGroupRect.top + 'px',
left: targetGroupRect.left + 'px',
bottom: targetGroupRect.bottom + 'px',
right: targetGroupRect.right + 'px',
background: 'orange'
});

dragElementViaMouse(fixture, element, targetRect.left + 1, targetRect.top + 1);
flush();
fixture.detectChanges();

const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0];

expect(event).toBeTruthy();
expect(event).toEqual({
previousIndex: 1,
currentIndex: 1,
item: groups[0][1],
container: dropInstances[0],
previousContainer: dropInstances[0],
isPointerOverContainer: false
});
}));

});

});
Expand Down
3 changes: 3 additions & 0 deletions src/cdk/drag-drop/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,9 @@ export class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
}

extendStyles(preview.style, {
// It's important that we disable the pointer events on the preview, because
// it can throw off the `document.elementFromPoint` calls in the `CdkDropList`.
pointerEvents: 'none',
position: 'fixed',
top: '0',
left: '0',
Expand Down
51 changes: 48 additions & 3 deletions src/cdk/drag-drop/drop-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import {
Directive,
ChangeDetectorRef,
SkipSelf,
Inject,
} from '@angular/core';
import {Directionality} from '@angular/cdk/bidi';
import {DOCUMENT} from '@angular/common';
import {CdkDrag} from './drag';
import {DragDropRegistry} from './drag-drop-registry';
import {CdkDragDrop, CdkDragEnter, CdkDragExit, CdkDragSortEvent} from './drag-events';
Expand Down Expand Up @@ -93,6 +95,8 @@ interface ListPositionCacheEntry {
}
})
export class CdkDropList<T = any> implements OnInit, OnDestroy {
private _document: Document | undefined;

/** Draggable items in the container. */
@ContentChildren(forwardRef(() => CdkDrag)) _draggables: QueryList<CdkDrag>;

Expand Down Expand Up @@ -160,7 +164,17 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
private _dragDropRegistry: DragDropRegistry<CdkDrag, CdkDropList<T>>,
private _changeDetectorRef: ChangeDetectorRef,
@Optional() private _dir?: Directionality,
@Optional() @SkipSelf() private _group?: CdkDropListGroup<CdkDropList>) {}
@Optional() @SkipSelf() private _group?: CdkDropListGroup<CdkDropList>,
// @breaking-change 8.0.0 `_document` parameter to be made required.
@Optional() @Inject(DOCUMENT) _document?: any) {

// @breaking-change 8.0.0 Remove null checks once `_document` parameter is required.
if (_document) {
this._document = _document;
} else if (typeof document !== 'undefined') {
this._document = document;
}
}

ngOnInit() {
this._dragDropRegistry.registerDropContainer(this);
Expand Down Expand Up @@ -384,8 +398,39 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
* @param y Position of the item along the Y axis.
*/
_getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number): CdkDropList | null {
const result = this._positionCache.siblings
.find(sibling => isInsideClientRect(sibling.clientRect, x, y));
const results = this._positionCache.siblings.filter(sibling => {
return isInsideClientRect(sibling.clientRect, x, y);
});

// No drop containers are intersecting with the pointer.
if (!results.length) {
return null;
}

let result: ListPositionCacheEntry | undefined = results[0];

// @breaking-change 8.0.0 remove null check once the
// `_document` is made into a required parameter.
if (this._document) {
const elementFromPoint = this._document.elementFromPoint(x, y);

// If there's no element at the pointer position, then
// the client rect is probably scrolled out of the view.
if (!elementFromPoint) {
return null;
}

// The `ClientRect`, that we're using to find the container over which the user is
// hovering, doesn't give us any information on whether the element has been scrolled
// out of the view or whether it's overlapping with other containers. This means that
// we could end up transferring the item into a container that's invisible or is positioned
// below another one. We use the result from `elementFromPoint` to get the top-most element
// at the pointer position and to find whether it's one of the intersecting drop containers.
result = results.find(sibling => {
const element = sibling.drop.element.nativeElement;
return element === elementFromPoint || element.contains(elementFromPoint);
});
}

return result && result.drop.enterPredicate(item, result.drop) ? result.drop : null;
}
Expand Down
2 changes: 1 addition & 1 deletion tools/public_api_guard/cdk/drag-drop.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export declare class CdkDropList<T = any> implements OnInit, OnDestroy {
lockAxis: 'x' | 'y';
orientation: 'horizontal' | 'vertical';
sorted: EventEmitter<CdkDragSortEvent<T>>;
constructor(element: ElementRef<HTMLElement>, _dragDropRegistry: DragDropRegistry<CdkDrag, CdkDropList<T>>, _changeDetectorRef: ChangeDetectorRef, _dir?: Directionality | undefined, _group?: CdkDropListGroup<CdkDropList<any>> | undefined);
constructor(element: ElementRef<HTMLElement>, _dragDropRegistry: DragDropRegistry<CdkDrag, CdkDropList<T>>, _changeDetectorRef: ChangeDetectorRef, _dir?: Directionality | undefined, _group?: CdkDropListGroup<CdkDropList<any>> | undefined, _document?: any);
_getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number): CdkDropList | null;
_isOverContainer(x: number, y: number): boolean;
_sortItem(item: CdkDrag, pointerX: number, pointerY: number, pointerDelta: {
Expand Down