Skip to content

fix(drag-drop): boundary not accounting for scrolling #18612

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
Apr 8, 2020
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
69 changes: 69 additions & 0 deletions src/cdk/drag-drop/client-rect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

/** Gets a mutable version of an element's bounding `ClientRect`. */
export function getMutableClientRect(element: Element): ClientRect {
const clientRect = element.getBoundingClientRect();

// We need to clone the `clientRect` here, because all the values on it are readonly
// and we need to be able to update them. Also we can't use a spread here, because
// the values on a `ClientRect` aren't own properties. See:
// https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect#Notes
return {
top: clientRect.top,
right: clientRect.right,
bottom: clientRect.bottom,
left: clientRect.left,
width: clientRect.width,
height: clientRect.height
};
}

/**
* Checks whether some coordinates are within a `ClientRect`.
* @param clientRect ClientRect that is being checked.
* @param x Coordinates along the X axis.
* @param y Coordinates along the Y axis.
*/
export function isInsideClientRect(clientRect: ClientRect, x: number, y: number) {
const {top, bottom, left, right} = clientRect;
return y >= top && y <= bottom && x >= left && x <= right;
}

/**
* Updates the top/left positions of a `ClientRect`, as well as their bottom/right counterparts.
* @param clientRect `ClientRect` that should be updated.
* @param top Amount to add to the `top` position.
* @param left Amount to add to the `left` position.
*/
export function adjustClientRect(clientRect: ClientRect, top: number, left: number) {
clientRect.top += top;
clientRect.bottom = clientRect.top + clientRect.height;

clientRect.left += left;
clientRect.right = clientRect.left + clientRect.width;
}

/**
* Checks whether the pointer coordinates are close to a ClientRect.
* @param rect ClientRect to check against.
* @param threshold Threshold around the ClientRect.
* @param pointerX Coordinates along the X axis.
* @param pointerY Coordinates along the Y axis.
*/
export function isPointerNearClientRect(rect: ClientRect,
threshold: number,
pointerX: number,
pointerY: number): boolean {
const {top, right, bottom, left, width, height} = rect;
const xThreshold = width * threshold;
const yThreshold = height * threshold;

return pointerY > top - yThreshold && pointerY < bottom + yThreshold &&
pointerX > left - xThreshold && pointerX < right + xThreshold;
}
33 changes: 33 additions & 0 deletions src/cdk/drag-drop/directives/drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2010,6 +2010,39 @@ describe('CdkDrag', () => {
expect(Math.floor(previewRect.right)).toBe(Math.floor(listRect.right));
}));

it('should update the boundary if the page is scrolled while dragging', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.componentInstance.boundarySelector = '.cdk-drop-list';
fixture.detectChanges();

const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
const list = fixture.componentInstance.dropInstance.element.nativeElement;
const cleanup = makePageScrollable();
scrollTo(0, 10);
let listRect = list.getBoundingClientRect(); // Note that we need to measure after scrolling.

startDraggingViaMouse(fixture, item);
startDraggingViaMouse(fixture, item, listRect.right, listRect.bottom);
flush();
dispatchMouseEvent(document, 'mousemove', listRect.right, listRect.bottom);
fixture.detectChanges();

const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
let previewRect = preview.getBoundingClientRect();
expect(Math.floor(previewRect.bottom)).toBe(Math.floor(listRect.bottom));

scrollTo(0, 0);
dispatchFakeEvent(document, 'scroll');
fixture.detectChanges();
listRect = list.getBoundingClientRect(); // We need to update these since we've scrolled.
dispatchMouseEvent(document, 'mousemove', listRect.right, listRect.bottom);
fixture.detectChanges();
previewRect = preview.getBoundingClientRect();

expect(Math.floor(previewRect.bottom)).toBe(Math.floor(listRect.bottom));
cleanup();
}));

it('should clear the id from the preview', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.detectChanges();
Expand Down
30 changes: 23 additions & 7 deletions src/cdk/drag-drop/drag-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {DropListRefInternal as DropListRef} from './drop-list-ref';
import {DragDropRegistry} from './drag-drop-registry';
import {extendStyles, toggleNativeDragInteractions} from './drag-styling';
import {getTransformTransitionDurationInMs} from './transition-duration';
import {getMutableClientRect, adjustClientRect} from './client-rect';

/** Object that can be used to configure the behavior of DragRef. */
export interface DragRefConfig {
Expand Down Expand Up @@ -490,7 +491,7 @@ export class DragRef<T = any> {
const position = this._pointerPositionAtLastDirectionChange;

if (position && this._dropContainer) {
this._updateActiveDropContainer(position);
this._updateActiveDropContainer(this._getConstrainedPointerPosition(position));
}
}

Expand Down Expand Up @@ -551,9 +552,9 @@ export class DragRef<T = any> {
// Prevent the default action as early as possible in order to block
// native actions like dragging the selected text or images with the mouse.
event.preventDefault();
const pointerPosition = this._getPointerPositionOnPage(event);

if (!this._hasStartedDragging) {
const pointerPosition = this._getPointerPositionOnPage(event);
const distanceX = Math.abs(pointerPosition.x - this._pickupPositionOnPage.x);
const distanceY = Math.abs(pointerPosition.y - this._pickupPositionOnPage.y);
const isOverThreshold = distanceX + distanceY >= this._config.dragStartThreshold;
Expand Down Expand Up @@ -590,7 +591,7 @@ export class DragRef<T = any> {
}
}

const constrainedPointerPosition = this._getConstrainedPointerPosition(event);
const constrainedPointerPosition = this._getConstrainedPointerPosition(pointerPosition);
this._hasMoved = true;
this._updatePointerDirectionDelta(constrainedPointerPosition);

Expand Down Expand Up @@ -770,11 +771,11 @@ export class DragRef<T = any> {
this._pointerMoveSubscription = this._dragDropRegistry.pointerMove.subscribe(this._pointerMove);
this._pointerUpSubscription = this._dragDropRegistry.pointerUp.subscribe(this._pointerUp);
this._scrollSubscription = this._dragDropRegistry.scroll.pipe(startWith(null)).subscribe(() => {
this._scrollPosition = this._viewportRuler.getViewportScrollPosition();
this._updateOnScroll();
});

if (this._boundaryElement) {
this._boundaryRect = this._boundaryElement.getBoundingClientRect();
this._boundaryRect = getMutableClientRect(this._boundaryElement);
}

// If we have a custom preview we can't know ahead of time how large it'll be so we position
Expand Down Expand Up @@ -1028,8 +1029,7 @@ export class DragRef<T = any> {


/** Gets the pointer position on the page, accounting for any position constraints. */
private _getConstrainedPointerPosition(event: MouseEvent | TouchEvent): Point {
const point = this._getPointerPositionOnPage(event);
private _getConstrainedPointerPosition(point: Point): Point {
const constrainedPoint = this.constrainPosition ? this.constrainPosition(point, this) : point;
const dropContainerLock = this._dropContainer ? this._dropContainer.lockAxis : null;

Expand Down Expand Up @@ -1214,6 +1214,22 @@ export class DragRef<T = any> {

return value ? value.mouse : 0;
}

/** Updates the internal state of the draggable element when scrolling has occurred. */
private _updateOnScroll() {
const oldScrollPosition = this._scrollPosition;
const currentScrollPosition = this._viewportRuler.getViewportScrollPosition();

// ClientRect dimensions are based on the page's scroll position so
// we have to update the cached boundary ClientRect if the user has scrolled.
if (oldScrollPosition && this._boundaryRect) {
const topDifference = oldScrollPosition.top - currentScrollPosition.top;
const leftDifference = oldScrollPosition.left - currentScrollPosition.left;
adjustClientRect(this._boundaryRect, topDifference, leftDifference);
}

this._scrollPosition = currentScrollPosition;
}
}

/**
Expand Down
72 changes: 10 additions & 62 deletions src/cdk/drag-drop/drop-list-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import {takeUntil} from 'rxjs/operators';
import {moveItemInArray} from './drag-utils';
import {DragDropRegistry} from './drag-drop-registry';
import {DragRefInternal as DragRef, Point} from './drag-ref';
import {
isPointerNearClientRect,
adjustClientRect,
getMutableClientRect,
isInsideClientRect,
} from './client-rect';

/**
* Proximity, as a ratio to width/height, at which a
Expand Down Expand Up @@ -446,7 +452,8 @@ export class DropListRef<T = any> {
_sortItem(item: DragRef, pointerX: number, pointerY: number,
pointerDelta: {x: number, y: number}): void {
// Don't sort the item if sorting is disabled or it's out of range.
if (this.sortingDisabled || !isPointerNearClientRect(this._clientRect, pointerX, pointerY)) {
if (this.sortingDisabled ||
!isPointerNearClientRect(this._clientRect, DROP_PROXIMITY_THRESHOLD, pointerX, pointerY)) {
return;
}

Expand Down Expand Up @@ -540,7 +547,8 @@ export class DropListRef<T = any> {
return;
}

if (isPointerNearClientRect(position.clientRect, pointerX, pointerY)) {
if (isPointerNearClientRect(position.clientRect, DROP_PROXIMITY_THRESHOLD,
pointerX, pointerY)) {
[verticalScrollDirection, horizontalScrollDirection] = getElementScrollDirections(
element as HTMLElement, position.clientRect, pointerX, pointerY);

Expand Down Expand Up @@ -921,35 +929,6 @@ export class DropListRef<T = any> {
}


/**
* Updates the top/left positions of a `ClientRect`, as well as their bottom/right counterparts.
* @param clientRect `ClientRect` that should be updated.
* @param top Amount to add to the `top` position.
* @param left Amount to add to the `left` position.
*/
function adjustClientRect(clientRect: ClientRect, top: number, left: number) {
clientRect.top += top;
clientRect.bottom = clientRect.top + clientRect.height;

clientRect.left += left;
clientRect.right = clientRect.left + clientRect.width;
}

/**
* Checks whether the pointer coordinates are close to a ClientRect.
* @param rect ClientRect to check against.
* @param pointerX Coordinates along the X axis.
* @param pointerY Coordinates along the Y axis.
*/
function isPointerNearClientRect(rect: ClientRect, pointerX: number, pointerY: number): boolean {
const {top, right, bottom, left, width, height} = rect;
const xThreshold = width * DROP_PROXIMITY_THRESHOLD;
const yThreshold = height * DROP_PROXIMITY_THRESHOLD;

return pointerY > top - yThreshold && pointerY < bottom + yThreshold &&
pointerX > left - xThreshold && pointerX < right + xThreshold;
}

/**
* Finds the index of an item that matches a predicate function. Used as an equivalent
* of `Array.prototype.findIndex` which isn't part of the standard Google typings.
Expand All @@ -968,37 +947,6 @@ function findIndex<T>(array: T[],
return -1;
}


/**
* Checks whether some coordinates are within a `ClientRect`.
* @param clientRect ClientRect that is being checked.
* @param x Coordinates along the X axis.
* @param y Coordinates along the Y axis.
*/
function isInsideClientRect(clientRect: ClientRect, x: number, y: number) {
const {top, bottom, left, right} = clientRect;
return y >= top && y <= bottom && x >= left && x <= right;
}


/** Gets a mutable version of an element's bounding `ClientRect`. */
function getMutableClientRect(element: Element): ClientRect {
const clientRect = element.getBoundingClientRect();

// We need to clone the `clientRect` here, because all the values on it are readonly
// and we need to be able to update them. Also we can't use a spread here, because
// the values on a `ClientRect` aren't own properties. See:
// https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect#Notes
return {
top: clientRect.top,
right: clientRect.right,
bottom: clientRect.bottom,
left: clientRect.left,
width: clientRect.width,
height: clientRect.height
};
}

/**
* Increments the vertical scroll position of a node.
* @param node Node whose scroll position should change.
Expand Down