Skip to content

Commit 9a0fba5

Browse files
authored
fix(drag-drop): boundary not accounting for scrolling (#18612)
When the user starts dragging, we cache the `ClientRect` of the boundary so we know the area around which to limit the preview. The problem is that we weren't updating the cached position which cause it to behave incorrectly if the user scrolled after they start dragging. These changes fix the issue by updating cached position. This ended up slightly more complicated than expected, because it has to interact with the auto scrolling. I've also moved around some utilities for dealing with `ClientRect` so that they can be reused. Fixes #18597.
1 parent db873ea commit 9a0fba5

File tree

4 files changed

+135
-69
lines changed

4 files changed

+135
-69
lines changed

src/cdk/drag-drop/client-rect.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
/** Gets a mutable version of an element's bounding `ClientRect`. */
10+
export function getMutableClientRect(element: Element): ClientRect {
11+
const clientRect = element.getBoundingClientRect();
12+
13+
// We need to clone the `clientRect` here, because all the values on it are readonly
14+
// and we need to be able to update them. Also we can't use a spread here, because
15+
// the values on a `ClientRect` aren't own properties. See:
16+
// https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect#Notes
17+
return {
18+
top: clientRect.top,
19+
right: clientRect.right,
20+
bottom: clientRect.bottom,
21+
left: clientRect.left,
22+
width: clientRect.width,
23+
height: clientRect.height
24+
};
25+
}
26+
27+
/**
28+
* Checks whether some coordinates are within a `ClientRect`.
29+
* @param clientRect ClientRect that is being checked.
30+
* @param x Coordinates along the X axis.
31+
* @param y Coordinates along the Y axis.
32+
*/
33+
export function isInsideClientRect(clientRect: ClientRect, x: number, y: number) {
34+
const {top, bottom, left, right} = clientRect;
35+
return y >= top && y <= bottom && x >= left && x <= right;
36+
}
37+
38+
/**
39+
* Updates the top/left positions of a `ClientRect`, as well as their bottom/right counterparts.
40+
* @param clientRect `ClientRect` that should be updated.
41+
* @param top Amount to add to the `top` position.
42+
* @param left Amount to add to the `left` position.
43+
*/
44+
export function adjustClientRect(clientRect: ClientRect, top: number, left: number) {
45+
clientRect.top += top;
46+
clientRect.bottom = clientRect.top + clientRect.height;
47+
48+
clientRect.left += left;
49+
clientRect.right = clientRect.left + clientRect.width;
50+
}
51+
52+
/**
53+
* Checks whether the pointer coordinates are close to a ClientRect.
54+
* @param rect ClientRect to check against.
55+
* @param threshold Threshold around the ClientRect.
56+
* @param pointerX Coordinates along the X axis.
57+
* @param pointerY Coordinates along the Y axis.
58+
*/
59+
export function isPointerNearClientRect(rect: ClientRect,
60+
threshold: number,
61+
pointerX: number,
62+
pointerY: number): boolean {
63+
const {top, right, bottom, left, width, height} = rect;
64+
const xThreshold = width * threshold;
65+
const yThreshold = height * threshold;
66+
67+
return pointerY > top - yThreshold && pointerY < bottom + yThreshold &&
68+
pointerX > left - xThreshold && pointerX < right + xThreshold;
69+
}

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2027,6 +2027,39 @@ describe('CdkDrag', () => {
20272027
expect(Math.floor(previewRect.right)).toBe(Math.floor(listRect.right));
20282028
}));
20292029

2030+
it('should update the boundary if the page is scrolled while dragging', fakeAsync(() => {
2031+
const fixture = createComponent(DraggableInDropZone);
2032+
fixture.componentInstance.boundarySelector = '.cdk-drop-list';
2033+
fixture.detectChanges();
2034+
2035+
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
2036+
const list = fixture.componentInstance.dropInstance.element.nativeElement;
2037+
const cleanup = makePageScrollable();
2038+
scrollTo(0, 10);
2039+
let listRect = list.getBoundingClientRect(); // Note that we need to measure after scrolling.
2040+
2041+
startDraggingViaMouse(fixture, item);
2042+
startDraggingViaMouse(fixture, item, listRect.right, listRect.bottom);
2043+
flush();
2044+
dispatchMouseEvent(document, 'mousemove', listRect.right, listRect.bottom);
2045+
fixture.detectChanges();
2046+
2047+
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
2048+
let previewRect = preview.getBoundingClientRect();
2049+
expect(Math.floor(previewRect.bottom)).toBe(Math.floor(listRect.bottom));
2050+
2051+
scrollTo(0, 0);
2052+
dispatchFakeEvent(document, 'scroll');
2053+
fixture.detectChanges();
2054+
listRect = list.getBoundingClientRect(); // We need to update these since we've scrolled.
2055+
dispatchMouseEvent(document, 'mousemove', listRect.right, listRect.bottom);
2056+
fixture.detectChanges();
2057+
previewRect = preview.getBoundingClientRect();
2058+
2059+
expect(Math.floor(previewRect.bottom)).toBe(Math.floor(listRect.bottom));
2060+
cleanup();
2061+
}));
2062+
20302063
it('should clear the id from the preview', fakeAsync(() => {
20312064
const fixture = createComponent(DraggableInDropZone);
20322065
fixture.detectChanges();

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

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {DropListRefInternal as DropListRef} from './drop-list-ref';
1717
import {DragDropRegistry} from './drag-drop-registry';
1818
import {extendStyles, toggleNativeDragInteractions} from './drag-styling';
1919
import {getTransformTransitionDurationInMs} from './transition-duration';
20+
import {getMutableClientRect, adjustClientRect} from './client-rect';
2021

2122
/** Object that can be used to configure the behavior of DragRef. */
2223
export interface DragRefConfig {
@@ -495,7 +496,7 @@ export class DragRef<T = any> {
495496
const position = this._pointerPositionAtLastDirectionChange;
496497

497498
if (position && this._dropContainer) {
498-
this._updateActiveDropContainer(position);
499+
this._updateActiveDropContainer(this._getConstrainedPointerPosition(position));
499500
}
500501
}
501502

@@ -556,9 +557,9 @@ export class DragRef<T = any> {
556557
// Prevent the default action as early as possible in order to block
557558
// native actions like dragging the selected text or images with the mouse.
558559
event.preventDefault();
560+
const pointerPosition = this._getPointerPositionOnPage(event);
559561

560562
if (!this._hasStartedDragging) {
561-
const pointerPosition = this._getPointerPositionOnPage(event);
562563
const distanceX = Math.abs(pointerPosition.x - this._pickupPositionOnPage.x);
563564
const distanceY = Math.abs(pointerPosition.y - this._pickupPositionOnPage.y);
564565
const isOverThreshold = distanceX + distanceY >= this._config.dragStartThreshold;
@@ -595,7 +596,7 @@ export class DragRef<T = any> {
595596
}
596597
}
597598

598-
const constrainedPointerPosition = this._getConstrainedPointerPosition(event);
599+
const constrainedPointerPosition = this._getConstrainedPointerPosition(pointerPosition);
599600
this._hasMoved = true;
600601
this._updatePointerDirectionDelta(constrainedPointerPosition);
601602

@@ -775,11 +776,11 @@ export class DragRef<T = any> {
775776
this._pointerMoveSubscription = this._dragDropRegistry.pointerMove.subscribe(this._pointerMove);
776777
this._pointerUpSubscription = this._dragDropRegistry.pointerUp.subscribe(this._pointerUp);
777778
this._scrollSubscription = this._dragDropRegistry.scroll.pipe(startWith(null)).subscribe(() => {
778-
this._scrollPosition = this._viewportRuler.getViewportScrollPosition();
779+
this._updateOnScroll();
779780
});
780781

781782
if (this._boundaryElement) {
782-
this._boundaryRect = this._boundaryElement.getBoundingClientRect();
783+
this._boundaryRect = getMutableClientRect(this._boundaryElement);
783784
}
784785

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

10341035

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

@@ -1219,6 +1219,22 @@ export class DragRef<T = any> {
12191219

12201220
return value ? value.mouse : 0;
12211221
}
1222+
1223+
/** Updates the internal state of the draggable element when scrolling has occurred. */
1224+
private _updateOnScroll() {
1225+
const oldScrollPosition = this._scrollPosition;
1226+
const currentScrollPosition = this._viewportRuler.getViewportScrollPosition();
1227+
1228+
// ClientRect dimensions are based on the page's scroll position so
1229+
// we have to update the cached boundary ClientRect if the user has scrolled.
1230+
if (oldScrollPosition && this._boundaryRect) {
1231+
const topDifference = oldScrollPosition.top - currentScrollPosition.top;
1232+
const leftDifference = oldScrollPosition.left - currentScrollPosition.left;
1233+
adjustClientRect(this._boundaryRect, topDifference, leftDifference);
1234+
}
1235+
1236+
this._scrollPosition = currentScrollPosition;
1237+
}
12221238
}
12231239

12241240
/**

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

Lines changed: 10 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ import {takeUntil} from 'rxjs/operators';
1616
import {moveItemInArray} from './drag-utils';
1717
import {DragDropRegistry} from './drag-drop-registry';
1818
import {DragRefInternal as DragRef, Point} from './drag-ref';
19+
import {
20+
isPointerNearClientRect,
21+
adjustClientRect,
22+
getMutableClientRect,
23+
isInsideClientRect,
24+
} from './client-rect';
1925

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

@@ -540,7 +547,8 @@ export class DropListRef<T = any> {
540547
return;
541548
}
542549

543-
if (isPointerNearClientRect(position.clientRect, pointerX, pointerY)) {
550+
if (isPointerNearClientRect(position.clientRect, DROP_PROXIMITY_THRESHOLD,
551+
pointerX, pointerY)) {
544552
[verticalScrollDirection, horizontalScrollDirection] = getElementScrollDirections(
545553
element as HTMLElement, position.clientRect, pointerX, pointerY);
546554

@@ -921,35 +929,6 @@ export class DropListRef<T = any> {
921929
}
922930

923931

924-
/**
925-
* Updates the top/left positions of a `ClientRect`, as well as their bottom/right counterparts.
926-
* @param clientRect `ClientRect` that should be updated.
927-
* @param top Amount to add to the `top` position.
928-
* @param left Amount to add to the `left` position.
929-
*/
930-
function adjustClientRect(clientRect: ClientRect, top: number, left: number) {
931-
clientRect.top += top;
932-
clientRect.bottom = clientRect.top + clientRect.height;
933-
934-
clientRect.left += left;
935-
clientRect.right = clientRect.left + clientRect.width;
936-
}
937-
938-
/**
939-
* Checks whether the pointer coordinates are close to a ClientRect.
940-
* @param rect ClientRect to check against.
941-
* @param pointerX Coordinates along the X axis.
942-
* @param pointerY Coordinates along the Y axis.
943-
*/
944-
function isPointerNearClientRect(rect: ClientRect, pointerX: number, pointerY: number): boolean {
945-
const {top, right, bottom, left, width, height} = rect;
946-
const xThreshold = width * DROP_PROXIMITY_THRESHOLD;
947-
const yThreshold = height * DROP_PROXIMITY_THRESHOLD;
948-
949-
return pointerY > top - yThreshold && pointerY < bottom + yThreshold &&
950-
pointerX > left - xThreshold && pointerX < right + xThreshold;
951-
}
952-
953932
/**
954933
* Finds the index of an item that matches a predicate function. Used as an equivalent
955934
* of `Array.prototype.findIndex` which isn't part of the standard Google typings.
@@ -968,37 +947,6 @@ function findIndex<T>(array: T[],
968947
return -1;
969948
}
970949

971-
972-
/**
973-
* Checks whether some coordinates are within a `ClientRect`.
974-
* @param clientRect ClientRect that is being checked.
975-
* @param x Coordinates along the X axis.
976-
* @param y Coordinates along the Y axis.
977-
*/
978-
function isInsideClientRect(clientRect: ClientRect, x: number, y: number) {
979-
const {top, bottom, left, right} = clientRect;
980-
return y >= top && y <= bottom && x >= left && x <= right;
981-
}
982-
983-
984-
/** Gets a mutable version of an element's bounding `ClientRect`. */
985-
function getMutableClientRect(element: Element): ClientRect {
986-
const clientRect = element.getBoundingClientRect();
987-
988-
// We need to clone the `clientRect` here, because all the values on it are readonly
989-
// and we need to be able to update them. Also we can't use a spread here, because
990-
// the values on a `ClientRect` aren't own properties. See:
991-
// https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect#Notes
992-
return {
993-
top: clientRect.top,
994-
right: clientRect.right,
995-
bottom: clientRect.bottom,
996-
left: clientRect.left,
997-
width: clientRect.width,
998-
height: clientRect.height
999-
};
1000-
}
1001-
1002950
/**
1003951
* Increments the vertical scroll position of a node.
1004952
* @param node Node whose scroll position should change.

0 commit comments

Comments
 (0)