Skip to content

Commit 09c020d

Browse files
committed
fix(drag-drop): boundary not accounting for parent scrolling
In #18597 some logic was added to update the boundary dimensions when the page is scrolled, however that logic didn't account for the case where a parent element is being scrolled. We were already handling parent elements for the drop list so these changes move the logic into a separate class so it can be reused and fix the issue for the drag item. Fixes #19086.
1 parent 447f2e6 commit 09c020d

File tree

5 files changed

+216
-132
lines changed

5 files changed

+216
-132
lines changed

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

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ describe('CdkDrag', () => {
9696
const fixture = createComponent(StandaloneDraggable);
9797
fixture.detectChanges();
9898

99-
const cleanup = makePageScrollable();
99+
const cleanup = makeScrollable();
100100
const dragElement = fixture.componentInstance.dragElement.nativeElement;
101101

102102
scrollTo(0, 500);
@@ -126,7 +126,7 @@ describe('CdkDrag', () => {
126126
fixture.detectChanges();
127127

128128
const dragElement = fixture.componentInstance.dragElement.nativeElement;
129-
const cleanup = makePageScrollable();
129+
const cleanup = makeScrollable();
130130

131131
scrollTo(0, 500);
132132
expect(dragElement.style.transform).toBeFalsy();
@@ -256,7 +256,7 @@ describe('CdkDrag', () => {
256256
fixture.detectChanges();
257257

258258
const dragElement = fixture.componentInstance.dragElement.nativeElement;
259-
const cleanup = makePageScrollable();
259+
const cleanup = makeScrollable();
260260

261261
scrollTo(0, 500);
262262
expect(dragElement.style.transform).toBeFalsy();
@@ -285,7 +285,7 @@ describe('CdkDrag', () => {
285285
fixture.detectChanges();
286286

287287
const dragElement = fixture.componentInstance.dragElement.nativeElement;
288-
const cleanup = makePageScrollable();
288+
const cleanup = makeScrollable();
289289

290290
scrollTo(0, 500);
291291
expect(dragElement.style.transform).toBeFalsy();
@@ -2034,7 +2034,7 @@ describe('CdkDrag', () => {
20342034

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

@@ -2060,6 +2060,43 @@ describe('CdkDrag', () => {
20602060
cleanup();
20612061
}));
20622062

2063+
it('should update the boundary if a parent is scrolled while dragging', fakeAsync(() => {
2064+
const fixture = createComponent(DraggableInScrollableParentContainer);
2065+
fixture.componentInstance.boundarySelector = '.cdk-drop-list';
2066+
fixture.detectChanges();
2067+
2068+
const container: HTMLElement = fixture.nativeElement.querySelector('.container');
2069+
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
2070+
const list = fixture.componentInstance.dropInstance.element.nativeElement;
2071+
const cleanup = makeScrollable('vertical', container);
2072+
container.scrollTop = 10;
2073+
let listRect = list.getBoundingClientRect(); // Note that we need to measure after scrolling.
2074+
2075+
startDraggingViaMouse(fixture, item);
2076+
startDraggingViaMouse(fixture, item, listRect.right, listRect.bottom);
2077+
flush();
2078+
dispatchMouseEvent(document, 'mousemove', listRect.right, listRect.bottom);
2079+
fixture.detectChanges();
2080+
2081+
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
2082+
let previewRect = preview.getBoundingClientRect();
2083+
2084+
// Different browsers round the scroll position differently so
2085+
// assert that the offsets are within a pixel of each other.
2086+
expect(Math.abs(previewRect.bottom - listRect.bottom)).toBeLessThan(2);
2087+
2088+
container.scrollTop = 0;
2089+
dispatchFakeEvent(container, 'scroll');
2090+
fixture.detectChanges();
2091+
listRect = list.getBoundingClientRect(); // We need to update these since we've scrolled.
2092+
dispatchMouseEvent(document, 'mousemove', listRect.right, listRect.bottom);
2093+
fixture.detectChanges();
2094+
previewRect = preview.getBoundingClientRect();
2095+
2096+
expect(Math.abs(previewRect.bottom - listRect.bottom)).toBeLessThan(2);
2097+
cleanup();
2098+
}));
2099+
20632100
it('should clear the id from the preview', fakeAsync(() => {
20642101
const fixture = createComponent(DraggableInDropZone);
20652102
fixture.detectChanges();
@@ -2375,7 +2412,7 @@ describe('CdkDrag', () => {
23752412
fakeAsync(() => {
23762413
const fixture = createComponent(DraggableInDropZone);
23772414
fixture.detectChanges();
2378-
const cleanup = makePageScrollable();
2415+
const cleanup = makeScrollable();
23792416

23802417
scrollTo(0, 500);
23812418
assertDownwardSorting(fixture, fixture.componentInstance.dragItems.map(item => {
@@ -2396,7 +2433,7 @@ describe('CdkDrag', () => {
23962433
fakeAsync(() => {
23972434
const fixture = createComponent(DraggableInDropZone);
23982435
fixture.detectChanges();
2399-
const cleanup = makePageScrollable();
2436+
const cleanup = makeScrollable();
24002437

24012438
scrollTo(0, 500);
24022439
assertUpwardSorting(fixture, fixture.componentInstance.dragItems.map(item => {
@@ -2893,7 +2930,7 @@ describe('CdkDrag', () => {
28932930
it('should keep the preview next to the trigger if the page was scrolled', fakeAsync(() => {
28942931
const fixture = createComponent(DraggableInDropZoneWithCustomPreview);
28952932
fixture.detectChanges();
2896-
const cleanup = makePageScrollable();
2933+
const cleanup = makeScrollable();
28972934
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
28982935

28992936
startDraggingViaMouse(fixture, item, 50, 50);
@@ -3468,7 +3505,7 @@ describe('CdkDrag', () => {
34683505
const fixture = createComponent(DraggableInDropZone);
34693506
fixture.detectChanges();
34703507

3471-
const cleanup = makePageScrollable();
3508+
const cleanup = makeScrollable();
34723509
const item = fixture.componentInstance.dragItems.first.element.nativeElement;
34733510
const viewportRuler = TestBed.inject(ViewportRuler);
34743511
const viewportSize = viewportRuler.getViewportSize();
@@ -3489,7 +3526,7 @@ describe('CdkDrag', () => {
34893526
const fixture = createComponent(DraggableInDropZone);
34903527
fixture.detectChanges();
34913528

3492-
const cleanup = makePageScrollable();
3529+
const cleanup = makeScrollable();
34933530
const item = fixture.componentInstance.dragItems.first.element.nativeElement;
34943531
const viewportRuler = TestBed.inject(ViewportRuler);
34953532
const viewportSize = viewportRuler.getViewportSize();
@@ -3512,7 +3549,7 @@ describe('CdkDrag', () => {
35123549
const fixture = createComponent(DraggableInDropZone);
35133550
fixture.detectChanges();
35143551

3515-
const cleanup = makePageScrollable('horizontal');
3552+
const cleanup = makeScrollable('horizontal');
35163553
const item = fixture.componentInstance.dragItems.first.element.nativeElement;
35173554
const viewportRuler = TestBed.inject(ViewportRuler);
35183555
const viewportSize = viewportRuler.getViewportSize();
@@ -3533,7 +3570,7 @@ describe('CdkDrag', () => {
35333570
const fixture = createComponent(DraggableInDropZone);
35343571
fixture.detectChanges();
35353572

3536-
const cleanup = makePageScrollable('horizontal');
3573+
const cleanup = makeScrollable('horizontal');
35373574
const item = fixture.componentInstance.dragItems.first.element.nativeElement;
35383575
const viewportRuler = TestBed.inject(ViewportRuler);
35393576
const viewportSize = viewportRuler.getViewportSize();
@@ -3570,7 +3607,7 @@ describe('CdkDrag', () => {
35703607
list.style.margin = '0';
35713608

35723609
const listRect = list.getBoundingClientRect();
3573-
const cleanup = makePageScrollable();
3610+
const cleanup = makeScrollable();
35743611

35753612
scrollTo(0, viewportRuler.getViewportSize().height * 5);
35763613
list.scrollTop = 50;
@@ -3608,7 +3645,7 @@ describe('CdkDrag', () => {
36083645
list.style.margin = '0';
36093646

36103647
const listRect = list.getBoundingClientRect();
3611-
const cleanup = makePageScrollable();
3648+
const cleanup = makeScrollable();
36123649

36133650
scrollTo(0, viewportRuler.getViewportSize().height * 5);
36143651
list.scrollTop = 0;
@@ -4684,7 +4721,7 @@ describe('CdkDrag', () => {
46844721
fixture.detectChanges();
46854722

46864723
// Make the page scrollable and scroll the items out of view.
4687-
const cleanup = makePageScrollable();
4724+
const cleanup = makeScrollable();
46884725
scrollTo(0, 4000);
46894726
dispatchFakeEvent(document, 'scroll');
46904727
fixture.detectChanges();
@@ -5890,11 +5927,13 @@ function getElementSibligsByPosition(element: Element, direction: 'top' | 'left'
58905927
* Adds a large element to the page in order to make it scrollable.
58915928
* @returns Function that should be used to clean up after the test is done.
58925929
*/
5893-
function makePageScrollable(direction: 'vertical' | 'horizontal' = 'vertical') {
5930+
function makeScrollable(
5931+
direction: 'vertical' | 'horizontal' = 'vertical',
5932+
element = document.body) {
58945933
const veryTallElement = document.createElement('div');
58955934
veryTallElement.style.width = direction === 'vertical' ? '100%' : '4000px';
58965935
veryTallElement.style.height = direction === 'vertical' ? '2000px' : '5px';
5897-
document.body.appendChild(veryTallElement);
5936+
element.appendChild(veryTallElement);
58985937

58995938
return () => {
59005939
scrollTo(0, 0);

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

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ import {Direction} from '@angular/cdk/bidi';
1212
import {normalizePassiveListenerOptions} from '@angular/cdk/platform';
1313
import {coerceBooleanProperty, coerceElement} from '@angular/cdk/coercion';
1414
import {Subscription, Subject, Observable} from 'rxjs';
15-
import {startWith} from 'rxjs/operators';
1615
import {DropListRefInternal as DropListRef} from './drop-list-ref';
1716
import {DragDropRegistry} from './drag-drop-registry';
1817
import {extendStyles, toggleNativeDragInteractions} from './drag-styling';
1918
import {getTransformTransitionDurationInMs} from './transition-duration';
2019
import {getMutableClientRect, adjustClientRect} from './client-rect';
20+
import {ParentPositionTracker} from './parent-position-tracker';
2121

2222
/** Object that can be used to configure the behavior of DragRef. */
2323
export interface DragRefConfig {
@@ -136,8 +136,8 @@ export class DragRef<T = any> {
136136
/** Index at which the item started in its initial container. */
137137
private _initialIndex: number;
138138

139-
/** Cached scroll position on the page when the element was picked up. */
140-
private _scrollPosition: {top: number, left: number};
139+
/** Cached positions of scrollable parent elements. */
140+
private _parentPositions: ParentPositionTracker;
141141

142142
/** Emits when the item is being moved. */
143143
private _moveEvents = new Subject<{
@@ -305,6 +305,7 @@ export class DragRef<T = any> {
305305
private _dragDropRegistry: DragDropRegistry<DragRef, DropListRef>) {
306306

307307
this.withRootElement(element);
308+
this._parentPositions = new ParentPositionTracker(_document, _viewportRuler);
308309
_dragDropRegistry.registerDragItem(this);
309310
}
310311

@@ -422,6 +423,7 @@ export class DragRef<T = any> {
422423
this._disabledHandles.clear();
423424
this._dropContainer = undefined;
424425
this._resizeSubscription.unsubscribe();
426+
this._parentPositions.clear();
425427
this._boundaryElement = this._rootElement = this._placeholderTemplate =
426428
this._previewTemplate = this._anchor = null!;
427429
}
@@ -702,7 +704,9 @@ export class DragRef<T = any> {
702704

703705
this._toggleNativeDragInteractions();
704706

705-
if (this._dropContainer) {
707+
const dropContainer = this._dropContainer;
708+
709+
if (dropContainer) {
706710
const element = this._rootElement;
707711
const parent = element.parentNode!;
708712
const preview = this._preview = this._createPreviewElement();
@@ -718,12 +722,16 @@ export class DragRef<T = any> {
718722
element.style.display = 'none';
719723
this._document.body.appendChild(parent.replaceChild(placeholder, element));
720724
getPreviewInsertionPoint(this._document).appendChild(preview);
721-
this._dropContainer.start();
722-
this._initialContainer = this._dropContainer;
723-
this._initialIndex = this._dropContainer.getItemIndex(this);
725+
dropContainer.start();
726+
this._initialContainer = dropContainer;
727+
this._initialIndex = dropContainer.getItemIndex(this);
724728
} else {
725729
this._initialContainer = this._initialIndex = undefined!;
726730
}
731+
732+
// Important to run after we've called `start` on the parent container
733+
// so that it has had time to resolve its scrollable parents.
734+
this._parentPositions.cache(dropContainer ? dropContainer.getScrollableParents() : []);
727735
}
728736

729737
/**
@@ -775,8 +783,8 @@ export class DragRef<T = any> {
775783
this._removeSubscriptions();
776784
this._pointerMoveSubscription = this._dragDropRegistry.pointerMove.subscribe(this._pointerMove);
777785
this._pointerUpSubscription = this._dragDropRegistry.pointerUp.subscribe(this._pointerUp);
778-
this._scrollSubscription = this._dragDropRegistry.scroll.pipe(startWith(null)).subscribe(() => {
779-
this._updateOnScroll();
786+
this._scrollSubscription = this._dragDropRegistry.scroll.subscribe(scrollEvent => {
787+
this._updateOnScroll(scrollEvent);
780788
});
781789

782790
if (this._boundaryElement) {
@@ -1012,8 +1020,9 @@ export class DragRef<T = any> {
10121020
const handleElement = referenceElement === this._rootElement ? null : referenceElement;
10131021
const referenceRect = handleElement ? handleElement.getBoundingClientRect() : elementRect;
10141022
const point = isTouchEvent(event) ? event.targetTouches[0] : event;
1015-
const x = point.pageX - referenceRect.left - this._scrollPosition.left;
1016-
const y = point.pageY - referenceRect.top - this._scrollPosition.top;
1023+
const scrollPosition = this._getViewportScrollPosition();
1024+
const x = point.pageX - referenceRect.left - scrollPosition.left;
1025+
const y = point.pageY - referenceRect.top - scrollPosition.top;
10171026

10181027
return {
10191028
x: referenceRect.left - elementRect.left + x,
@@ -1025,10 +1034,11 @@ export class DragRef<T = any> {
10251034
private _getPointerPositionOnPage(event: MouseEvent | TouchEvent): Point {
10261035
// `touches` will be empty for start/end events so we have to fall back to `changedTouches`.
10271036
const point = isTouchEvent(event) ? (event.touches[0] || event.changedTouches[0]) : event;
1037+
const scrollPosition = this._getViewportScrollPosition();
10281038

10291039
return {
1030-
x: point.pageX - this._scrollPosition.left,
1031-
y: point.pageY - this._scrollPosition.top
1040+
x: point.pageX - scrollPosition.left,
1041+
y: point.pageY - scrollPosition.top
10321042
};
10331043
}
10341044

@@ -1146,6 +1156,7 @@ export class DragRef<T = any> {
11461156
/** Cleans up any cached element dimensions that we don't need after dragging has stopped. */
11471157
private _cleanupCachedDimensions() {
11481158
this._boundaryRect = this._previewRect = undefined;
1159+
this._parentPositions.clear();
11491160
}
11501161

11511162
/**
@@ -1221,19 +1232,21 @@ export class DragRef<T = any> {
12211232
}
12221233

12231234
/** 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();
1235+
private _updateOnScroll(event: Event) {
1236+
const scrollDifference = this._parentPositions.handleScroll(event);
12271237

12281238
// ClientRect dimensions are based on the page's scroll position so
12291239
// 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);
1240+
if (this._boundaryRect && scrollDifference) {
1241+
adjustClientRect(this._boundaryRect, scrollDifference.top, scrollDifference.left);
12341242
}
1243+
}
12351244

1236-
this._scrollPosition = currentScrollPosition;
1245+
/** Gets the scroll position of the viewport. */
1246+
private _getViewportScrollPosition() {
1247+
const cachedPosition = this._parentPositions.positions.get(this._document);
1248+
return cachedPosition ? cachedPosition.scrollPosition :
1249+
this._viewportRuler.getViewportScrollPosition();
12371250
}
12381251
}
12391252

0 commit comments

Comments
 (0)