Skip to content

Commit 9604822

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 9604822

File tree

5 files changed

+213
-132
lines changed

5 files changed

+213
-132
lines changed

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

Lines changed: 53 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,40 @@ 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+
expect(Math.floor(previewRect.bottom)).toBe(Math.floor(listRect.bottom));
2084+
2085+
container.scrollTop = 0;
2086+
dispatchFakeEvent(container, 'scroll');
2087+
fixture.detectChanges();
2088+
listRect = list.getBoundingClientRect(); // We need to update these since we've scrolled.
2089+
dispatchMouseEvent(document, 'mousemove', listRect.right, listRect.bottom);
2090+
fixture.detectChanges();
2091+
previewRect = preview.getBoundingClientRect();
2092+
2093+
expect(Math.floor(previewRect.bottom)).toBe(Math.floor(listRect.bottom));
2094+
cleanup();
2095+
}));
2096+
20632097
it('should clear the id from the preview', fakeAsync(() => {
20642098
const fixture = createComponent(DraggableInDropZone);
20652099
fixture.detectChanges();
@@ -2375,7 +2409,7 @@ describe('CdkDrag', () => {
23752409
fakeAsync(() => {
23762410
const fixture = createComponent(DraggableInDropZone);
23772411
fixture.detectChanges();
2378-
const cleanup = makePageScrollable();
2412+
const cleanup = makeScrollable();
23792413

23802414
scrollTo(0, 500);
23812415
assertDownwardSorting(fixture, fixture.componentInstance.dragItems.map(item => {
@@ -2396,7 +2430,7 @@ describe('CdkDrag', () => {
23962430
fakeAsync(() => {
23972431
const fixture = createComponent(DraggableInDropZone);
23982432
fixture.detectChanges();
2399-
const cleanup = makePageScrollable();
2433+
const cleanup = makeScrollable();
24002434

24012435
scrollTo(0, 500);
24022436
assertUpwardSorting(fixture, fixture.componentInstance.dragItems.map(item => {
@@ -2893,7 +2927,7 @@ describe('CdkDrag', () => {
28932927
it('should keep the preview next to the trigger if the page was scrolled', fakeAsync(() => {
28942928
const fixture = createComponent(DraggableInDropZoneWithCustomPreview);
28952929
fixture.detectChanges();
2896-
const cleanup = makePageScrollable();
2930+
const cleanup = makeScrollable();
28972931
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
28982932

28992933
startDraggingViaMouse(fixture, item, 50, 50);
@@ -3468,7 +3502,7 @@ describe('CdkDrag', () => {
34683502
const fixture = createComponent(DraggableInDropZone);
34693503
fixture.detectChanges();
34703504

3471-
const cleanup = makePageScrollable();
3505+
const cleanup = makeScrollable();
34723506
const item = fixture.componentInstance.dragItems.first.element.nativeElement;
34733507
const viewportRuler = TestBed.inject(ViewportRuler);
34743508
const viewportSize = viewportRuler.getViewportSize();
@@ -3489,7 +3523,7 @@ describe('CdkDrag', () => {
34893523
const fixture = createComponent(DraggableInDropZone);
34903524
fixture.detectChanges();
34913525

3492-
const cleanup = makePageScrollable();
3526+
const cleanup = makeScrollable();
34933527
const item = fixture.componentInstance.dragItems.first.element.nativeElement;
34943528
const viewportRuler = TestBed.inject(ViewportRuler);
34953529
const viewportSize = viewportRuler.getViewportSize();
@@ -3512,7 +3546,7 @@ describe('CdkDrag', () => {
35123546
const fixture = createComponent(DraggableInDropZone);
35133547
fixture.detectChanges();
35143548

3515-
const cleanup = makePageScrollable('horizontal');
3549+
const cleanup = makeScrollable('horizontal');
35163550
const item = fixture.componentInstance.dragItems.first.element.nativeElement;
35173551
const viewportRuler = TestBed.inject(ViewportRuler);
35183552
const viewportSize = viewportRuler.getViewportSize();
@@ -3533,7 +3567,7 @@ describe('CdkDrag', () => {
35333567
const fixture = createComponent(DraggableInDropZone);
35343568
fixture.detectChanges();
35353569

3536-
const cleanup = makePageScrollable('horizontal');
3570+
const cleanup = makeScrollable('horizontal');
35373571
const item = fixture.componentInstance.dragItems.first.element.nativeElement;
35383572
const viewportRuler = TestBed.inject(ViewportRuler);
35393573
const viewportSize = viewportRuler.getViewportSize();
@@ -3570,7 +3604,7 @@ describe('CdkDrag', () => {
35703604
list.style.margin = '0';
35713605

35723606
const listRect = list.getBoundingClientRect();
3573-
const cleanup = makePageScrollable();
3607+
const cleanup = makeScrollable();
35743608

35753609
scrollTo(0, viewportRuler.getViewportSize().height * 5);
35763610
list.scrollTop = 50;
@@ -3608,7 +3642,7 @@ describe('CdkDrag', () => {
36083642
list.style.margin = '0';
36093643

36103644
const listRect = list.getBoundingClientRect();
3611-
const cleanup = makePageScrollable();
3645+
const cleanup = makeScrollable();
36123646

36133647
scrollTo(0, viewportRuler.getViewportSize().height * 5);
36143648
list.scrollTop = 0;
@@ -4684,7 +4718,7 @@ describe('CdkDrag', () => {
46844718
fixture.detectChanges();
46854719

46864720
// Make the page scrollable and scroll the items out of view.
4687-
const cleanup = makePageScrollable();
4721+
const cleanup = makeScrollable();
46884722
scrollTo(0, 4000);
46894723
dispatchFakeEvent(document, 'scroll');
46904724
fixture.detectChanges();
@@ -5890,11 +5924,13 @@ function getElementSibligsByPosition(element: Element, direction: 'top' | 'left'
58905924
* Adds a large element to the page in order to make it scrollable.
58915925
* @returns Function that should be used to clean up after the test is done.
58925926
*/
5893-
function makePageScrollable(direction: 'vertical' | 'horizontal' = 'vertical') {
5927+
function makeScrollable(
5928+
direction: 'vertical' | 'horizontal' = 'vertical',
5929+
element = document.body) {
58945930
const veryTallElement = document.createElement('div');
58955931
veryTallElement.style.width = direction === 'vertical' ? '100%' : '4000px';
58965932
veryTallElement.style.height = direction === 'vertical' ? '2000px' : '5px';
5897-
document.body.appendChild(veryTallElement);
5933+
element.appendChild(veryTallElement);
58985934

58995935
return () => {
59005936
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)