Skip to content

Commit a6e28db

Browse files
crisbetojosephperrott
authored andcommitted
fix(drag-drop): not accounting for scroll position when dragging (#12098)
1 parent 5c41410 commit a6e28db

File tree

4 files changed

+186
-51
lines changed

4 files changed

+186
-51
lines changed

src/cdk-experimental/drag-drop/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ ng_module(
1212
deps = [
1313
"@rxjs",
1414
"//src/cdk/platform",
15+
"//src/cdk/overlay",
1516
],
1617
tsconfig = "//src/cdk-experimental:tsconfig-build.json",
1718
)

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

Lines changed: 164 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,21 @@ describe('CdkDrag', () => {
3939
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
4040
}));
4141

42+
it('should drag an element freely to a particular position when the page is scrolled',
43+
fakeAsync(() => {
44+
const fixture = createComponent(StandaloneDraggable);
45+
fixture.detectChanges();
46+
47+
const cleanup = makePageScrollable();
48+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
49+
50+
scrollTo(0, 500);
51+
expect(dragElement.style.transform).toBeFalsy();
52+
dragElementViaMouse(fixture, dragElement, 50, 100);
53+
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
54+
cleanup();
55+
}));
56+
4257
it('should continue dragging the element from where it was left off', fakeAsync(() => {
4358
const fixture = createComponent(StandaloneDraggable);
4459
fixture.detectChanges();
@@ -52,6 +67,26 @@ describe('CdkDrag', () => {
5267
dragElementViaMouse(fixture, dragElement, 100, 200);
5368
expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)');
5469
}));
70+
71+
it('should continue dragging from where it was left off when the page is scrolled',
72+
fakeAsync(() => {
73+
const fixture = createComponent(StandaloneDraggable);
74+
fixture.detectChanges();
75+
76+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
77+
const cleanup = makePageScrollable();
78+
79+
scrollTo(0, 500);
80+
expect(dragElement.style.transform).toBeFalsy();
81+
82+
dragElementViaMouse(fixture, dragElement, 50, 100);
83+
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
84+
85+
dragElementViaMouse(fixture, dragElement, 100, 200);
86+
expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)');
87+
88+
cleanup();
89+
}));
5590
});
5691

5792
describe('touch dragging', () => {
@@ -65,6 +100,21 @@ describe('CdkDrag', () => {
65100
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
66101
}));
67102

103+
it('should drag an element freely to a particular position when the page is scrolled',
104+
fakeAsync(() => {
105+
const fixture = createComponent(StandaloneDraggable);
106+
fixture.detectChanges();
107+
108+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
109+
const cleanup = makePageScrollable();
110+
111+
scrollTo(0, 500);
112+
expect(dragElement.style.transform).toBeFalsy();
113+
dragElementViaTouch(fixture, dragElement, 50, 100);
114+
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
115+
cleanup();
116+
}));
117+
68118
it('should continue dragging the element from where it was left off', fakeAsync(() => {
69119
const fixture = createComponent(StandaloneDraggable);
70120
fixture.detectChanges();
@@ -79,6 +129,26 @@ describe('CdkDrag', () => {
79129
expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)');
80130
}));
81131

132+
it('should continue dragging from where it was left off when the page is scrolled',
133+
fakeAsync(() => {
134+
const fixture = createComponent(StandaloneDraggable);
135+
fixture.detectChanges();
136+
137+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
138+
const cleanup = makePageScrollable();
139+
140+
scrollTo(0, 500);
141+
expect(dragElement.style.transform).toBeFalsy();
142+
143+
dragElementViaTouch(fixture, dragElement, 50, 100);
144+
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
145+
146+
dragElementViaTouch(fixture, dragElement, 100, 200);
147+
expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)');
148+
149+
cleanup();
150+
}));
151+
82152
it('should prevent the default `touchmove` action on the page while dragging',
83153
fakeAsync(() => {
84154
const fixture = createComponent(StandaloneDraggable);
@@ -257,58 +327,36 @@ describe('CdkDrag', () => {
257327
it('should move the placeholder as an item is being sorted down', fakeAsync(() => {
258328
const fixture = createComponent(DraggableInDropZone);
259329
fixture.detectChanges();
330+
assertDownwardSorting(fixture);
331+
}));
260332

261-
const items = fixture.componentInstance.dragItems.toArray();
262-
const draggedItem = items[0].element.nativeElement;
263-
const {top, left} = draggedItem.getBoundingClientRect();
264-
265-
dispatchMouseEvent(draggedItem, 'mousedown', left, top);
266-
fixture.detectChanges();
267-
268-
const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement;
269-
270-
// Drag over each item one-by-one going downwards.
271-
for (let i = 0; i < items.length; i++) {
272-
const elementRect = items[i].element.nativeElement.getBoundingClientRect();
273-
274-
// Add a few pixels to the top offset so we get some overlap.
275-
dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.top + 5);
333+
it('should move the placeholder as an item is being sorted down on a scrolled page',
334+
fakeAsync(() => {
335+
const fixture = createComponent(DraggableInDropZone);
276336
fixture.detectChanges();
277-
expect(getElementIndex(placeholder)).toBe(i);
278-
}
337+
const cleanup = makePageScrollable();
279338

280-
dispatchMouseEvent(document, 'mouseup');
281-
fixture.detectChanges();
282-
flush();
283-
}));
339+
scrollTo(0, 500);
340+
assertDownwardSorting(fixture);
341+
cleanup();
342+
}));
284343

285344
it('should move the placeholder as an item is being sorted up', fakeAsync(() => {
286345
const fixture = createComponent(DraggableInDropZone);
287346
fixture.detectChanges();
347+
assertUpwardSorting(fixture);
348+
}));
288349

289-
const items = fixture.componentInstance.dragItems.toArray();
290-
const draggedItem = items[items.length - 1].element.nativeElement;
291-
const {top, left} = draggedItem.getBoundingClientRect();
292-
293-
dispatchMouseEvent(draggedItem, 'mousedown', left, top);
294-
fixture.detectChanges();
295-
296-
const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement;
297-
298-
// Drag over each item one-by-one going upwards.
299-
for (let i = items.length - 1; i > -1; i--) {
300-
const elementRect = items[i].element.nativeElement.getBoundingClientRect();
301-
302-
// Remove a few pixels from the bottom offset so we get some overlap.
303-
dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.bottom - 5);
350+
it('should move the placeholder as an item is being sorted up on a scrolled page',
351+
fakeAsync(() => {
352+
const fixture = createComponent(DraggableInDropZone);
304353
fixture.detectChanges();
305-
expect(getElementIndex(placeholder)).toBe(Math.min(i + 1, items.length - 1));
306-
}
354+
const cleanup = makePageScrollable();
307355

308-
dispatchMouseEvent(document, 'mouseup');
309-
fixture.detectChanges();
310-
flush();
311-
}));
356+
scrollTo(0, 500);
357+
assertUpwardSorting(fixture);
358+
cleanup();
359+
}));
312360

313361
it('should clean up the preview element if the item is destroyed mid-drag', fakeAsync(() => {
314362
const fixture = createComponent(DraggableInDropZone);
@@ -580,3 +628,77 @@ function dragElementViaTouch(fixture: ComponentFixture<any>,
580628
function getElementIndex(element: HTMLElement) {
581629
return element.parentElement ? Array.from(element.parentElement.children).indexOf(element) : -1;
582630
}
631+
632+
/**
633+
* Adds a large element to the page in order to make it scrollable.
634+
* @returns Function that should be used to clean up after the test is done.
635+
*/
636+
function makePageScrollable() {
637+
const veryTallElement = document.createElement('div');
638+
veryTallElement.style.width = '100%';
639+
veryTallElement.style.height = '2000px';
640+
document.body.appendChild(veryTallElement);
641+
642+
return () => {
643+
scrollTo(0, 0);
644+
veryTallElement.parentNode!.removeChild(veryTallElement);
645+
};
646+
}
647+
648+
/**
649+
* Asserts that sorting an element down works correctly.
650+
* @param fixture Fixture against which to run the assertions.
651+
*/
652+
function assertDownwardSorting(fixture: ComponentFixture<DraggableInDropZone>) {
653+
const items = fixture.componentInstance.dragItems.toArray();
654+
const draggedItem = items[0].element.nativeElement;
655+
const {top, left} = draggedItem.getBoundingClientRect();
656+
657+
dispatchMouseEvent(draggedItem, 'mousedown', left, top);
658+
fixture.detectChanges();
659+
660+
const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement;
661+
662+
// Drag over each item one-by-one going downwards.
663+
for (let i = 0; i < items.length; i++) {
664+
const elementRect = items[i].element.nativeElement.getBoundingClientRect();
665+
666+
// Add a few pixels to the top offset so we get some overlap.
667+
dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.top + 5);
668+
fixture.detectChanges();
669+
expect(getElementIndex(placeholder)).toBe(i);
670+
}
671+
672+
dispatchMouseEvent(document, 'mouseup');
673+
fixture.detectChanges();
674+
flush();
675+
}
676+
677+
/**
678+
* Asserts that sorting an element up works correctly.
679+
* @param fixture Fixture against which to run the assertions.
680+
*/
681+
function assertUpwardSorting(fixture: ComponentFixture<DraggableInDropZone>) {
682+
const items = fixture.componentInstance.dragItems.toArray();
683+
const draggedItem = items[items.length - 1].element.nativeElement;
684+
const {top, left} = draggedItem.getBoundingClientRect();
685+
686+
dispatchMouseEvent(draggedItem, 'mousedown', left, top);
687+
fixture.detectChanges();
688+
689+
const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement;
690+
691+
// Drag over each item one-by-one going upwards.
692+
for (let i = items.length - 1; i > -1; i--) {
693+
const elementRect = items[i].element.nativeElement.getBoundingClientRect();
694+
695+
// Remove a few pixels from the bottom offset so we get some overlap.
696+
dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.bottom - 5);
697+
fixture.detectChanges();
698+
expect(getElementIndex(placeholder)).toBe(Math.min(i + 1, items.length - 1));
699+
}
700+
701+
dispatchMouseEvent(document, 'mouseup');
702+
fixture.detectChanges();
703+
flush();
704+
}

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

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {supportsPassiveEventListeners} from '@angular/cdk/platform';
2828
import {CdkDragStart, CdkDragEnd, CdkDragExit, CdkDragEnter, CdkDragDrop} from './drag-events';
2929
import {CdkDragPreview} from './drag-preview';
3030
import {CdkDragPlaceholder} from './drag-placeholder';
31+
import {ViewportRuler} from '@angular/cdk/overlay';
3132

3233
/** Event options that can be used to bind an active event. */
3334
const activeEventOptions = supportsPassiveEventListeners() ? {passive: false} : false;
@@ -84,6 +85,9 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
8485
/** Drop container in which the CdkDrag resided when dragging began. */
8586
private _initialContainer: CdkDropContainer;
8687

88+
/** Cached scroll position on the page when the element was picked up. */
89+
private _scrollPosition: {top: number, left: number};
90+
8791
/** Element that can be used to drag the draggable item. */
8892
@ContentChild(CdkDragHandle) _handle: CdkDragHandle;
8993

@@ -117,7 +121,8 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
117121
@Inject(CDK_DROP_CONTAINER) @Optional() @SkipSelf() public dropContainer: CdkDropContainer,
118122
@Inject(DOCUMENT) document: any,
119123
private _ngZone: NgZone,
120-
private _viewContainerRef: ViewContainerRef) {
124+
private _viewContainerRef: ViewContainerRef,
125+
private _viewportRuler: ViewportRuler) {
121126
this._document = document;
122127
}
123128

@@ -135,7 +140,7 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
135140
dragElement.addEventListener('mousedown', this._pointerDown);
136141
dragElement.addEventListener('touchstart', this._pointerDown);
137142

138-
// Webkit won't preventDefault on a dynamically-added `touchmove` listener, which means that
143+
// WebKit won't preventDefault on a dynamically-added `touchmove` listener, which means that
139144
// we need to add one ahead of time. See https://bugs.webkit.org/show_bug.cgi?id=184250.
140145
// TODO: move into a central registry.
141146
this._ngZone.runOutsideAngular(() => {
@@ -165,6 +170,7 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
165170

166171
this._isDragging = true;
167172
this._initialContainer = this.dropContainer;
173+
this._scrollPosition = this._viewportRuler.getViewportScrollPosition();
168174

169175
// If we have a custom preview template, the element won't be visible anyway so we avoid the
170176
// extra `getBoundingClientRect` calls and just move the preview next to the cursor.
@@ -371,10 +377,12 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
371377
const elementRect = this.element.nativeElement.getBoundingClientRect();
372378
const handleElement = this._handle ? this._handle.element.nativeElement : null;
373379
const referenceRect = handleElement ? handleElement.getBoundingClientRect() : elementRect;
374-
const x = this._isTouchEvent(event) ? event.targetTouches[0].pageX - referenceRect.left :
375-
event.offsetX;
376-
const y = this._isTouchEvent(event) ? event.targetTouches[0].pageY - referenceRect.top :
377-
event.offsetY;
380+
const x = this._isTouchEvent(event) ?
381+
event.targetTouches[0].pageX - referenceRect.left - this._scrollPosition.left :
382+
event.offsetX;
383+
const y = this._isTouchEvent(event) ?
384+
event.targetTouches[0].pageY - referenceRect.top - this._scrollPosition.top :
385+
event.offsetY;
378386

379387
return {
380388
x: referenceRect.left - elementRect.left + x,
@@ -456,8 +464,12 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
456464

457465
/** Determines the point of the page that was touched by the user. */
458466
private _getPointerPositionOnPage(event: MouseEvent | TouchEvent): Point {
459-
return this._isTouchEvent(event) ? {x: event.touches[0].pageX, y: event.touches[0].pageY} :
460-
{x: event.pageX, y: event.pageY};
467+
const point = this._isTouchEvent(event) ? event.touches[0] : event;
468+
469+
return {
470+
x: point.pageX - this._scrollPosition.left,
471+
y: point.pageY - this._scrollPosition.top
472+
};
461473
}
462474

463475
/** Listener used to prevent `touchmove` events while the element is being dragged. */

src/cdk-experimental/drag-drop/drop.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
$cdk-z-index-drag-preview: 1000;
22

33
.cdk-drag-preview {
4-
position: absolute;
4+
position: fixed;
55
top: 0;
66
left: 0;
77
z-index: $cdk-z-index-drag-preview;

0 commit comments

Comments
 (0)