Skip to content

Commit 0bf1a07

Browse files
authored
fix(drag-drop): boundary not accounting for parent scrolling (#19108)
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 4eef958 commit 0bf1a07

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);
@@ -3485,7 +3522,7 @@ describe('CdkDrag', () => {
34853522
const fixture = createComponent(DraggableInDropZone);
34863523
fixture.detectChanges();
34873524

3488-
const cleanup = makePageScrollable();
3525+
const cleanup = makeScrollable();
34893526
const item = fixture.componentInstance.dragItems.first.element.nativeElement;
34903527
const viewportRuler = TestBed.inject(ViewportRuler);
34913528
const viewportSize = viewportRuler.getViewportSize();
@@ -3506,7 +3543,7 @@ describe('CdkDrag', () => {
35063543
const fixture = createComponent(DraggableInDropZone);
35073544
fixture.detectChanges();
35083545

3509-
const cleanup = makePageScrollable();
3546+
const cleanup = makeScrollable();
35103547
const item = fixture.componentInstance.dragItems.first.element.nativeElement;
35113548
const viewportRuler = TestBed.inject(ViewportRuler);
35123549
const viewportSize = viewportRuler.getViewportSize();
@@ -3529,7 +3566,7 @@ describe('CdkDrag', () => {
35293566
const fixture = createComponent(DraggableInDropZone);
35303567
fixture.detectChanges();
35313568

3532-
const cleanup = makePageScrollable('horizontal');
3569+
const cleanup = makeScrollable('horizontal');
35333570
const item = fixture.componentInstance.dragItems.first.element.nativeElement;
35343571
const viewportRuler = TestBed.inject(ViewportRuler);
35353572
const viewportSize = viewportRuler.getViewportSize();
@@ -3550,7 +3587,7 @@ describe('CdkDrag', () => {
35503587
const fixture = createComponent(DraggableInDropZone);
35513588
fixture.detectChanges();
35523589

3553-
const cleanup = makePageScrollable('horizontal');
3590+
const cleanup = makeScrollable('horizontal');
35543591
const item = fixture.componentInstance.dragItems.first.element.nativeElement;
35553592
const viewportRuler = TestBed.inject(ViewportRuler);
35563593
const viewportSize = viewportRuler.getViewportSize();
@@ -3587,7 +3624,7 @@ describe('CdkDrag', () => {
35873624
list.style.margin = '0';
35883625

35893626
const listRect = list.getBoundingClientRect();
3590-
const cleanup = makePageScrollable();
3627+
const cleanup = makeScrollable();
35913628

35923629
scrollTo(0, viewportRuler.getViewportSize().height * 5);
35933630
list.scrollTop = 50;
@@ -3625,7 +3662,7 @@ describe('CdkDrag', () => {
36253662
list.style.margin = '0';
36263663

36273664
const listRect = list.getBoundingClientRect();
3628-
const cleanup = makePageScrollable();
3665+
const cleanup = makeScrollable();
36293666

36303667
scrollTo(0, viewportRuler.getViewportSize().height * 5);
36313668
list.scrollTop = 0;
@@ -4744,7 +4781,7 @@ describe('CdkDrag', () => {
47444781
fixture.detectChanges();
47454782

47464783
// Make the page scrollable and scroll the items out of view.
4747-
const cleanup = makePageScrollable();
4784+
const cleanup = makeScrollable();
47484785
scrollTo(0, 4000);
47494786
dispatchFakeEvent(document, 'scroll');
47504787
fixture.detectChanges();
@@ -5984,11 +6021,13 @@ function getElementSibligsByPosition(element: Element, direction: 'top' | 'left'
59846021
* Adds a large element to the page in order to make it scrollable.
59856022
* @returns Function that should be used to clean up after the test is done.
59866023
*/
5987-
function makePageScrollable(direction: 'vertical' | 'horizontal' = 'vertical') {
6024+
function makeScrollable(
6025+
direction: 'vertical' | 'horizontal' = 'vertical',
6026+
element = document.body) {
59886027
const veryTallElement = document.createElement('div');
59896028
veryTallElement.style.width = direction === 'vertical' ? '100%' : '4000px';
59906029
veryTallElement.style.height = direction === 'vertical' ? '2000px' : '5px';
5991-
document.body.appendChild(veryTallElement);
6030+
element.appendChild(veryTallElement);
59926031

59936032
return () => {
59946033
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) {
@@ -1014,8 +1022,9 @@ export class DragRef<T = any> {
10141022
const handleElement = referenceElement === this._rootElement ? null : referenceElement;
10151023
const referenceRect = handleElement ? handleElement.getBoundingClientRect() : elementRect;
10161024
const point = isTouchEvent(event) ? event.targetTouches[0] : event;
1017-
const x = point.pageX - referenceRect.left - this._scrollPosition.left;
1018-
const y = point.pageY - referenceRect.top - this._scrollPosition.top;
1025+
const scrollPosition = this._getViewportScrollPosition();
1026+
const x = point.pageX - referenceRect.left - scrollPosition.left;
1027+
const y = point.pageY - referenceRect.top - scrollPosition.top;
10191028

10201029
return {
10211030
x: referenceRect.left - elementRect.left + x,
@@ -1027,10 +1036,11 @@ export class DragRef<T = any> {
10271036
private _getPointerPositionOnPage(event: MouseEvent | TouchEvent): Point {
10281037
// `touches` will be empty for start/end events so we have to fall back to `changedTouches`.
10291038
const point = isTouchEvent(event) ? (event.touches[0] || event.changedTouches[0]) : event;
1039+
const scrollPosition = this._getViewportScrollPosition();
10301040

10311041
return {
1032-
x: point.pageX - this._scrollPosition.left,
1033-
y: point.pageY - this._scrollPosition.top
1042+
x: point.pageX - scrollPosition.left,
1043+
y: point.pageY - scrollPosition.top
10341044
};
10351045
}
10361046

@@ -1148,6 +1158,7 @@ export class DragRef<T = any> {
11481158
/** Cleans up any cached element dimensions that we don't need after dragging has stopped. */
11491159
private _cleanupCachedDimensions() {
11501160
this._boundaryRect = this._previewRect = undefined;
1161+
this._parentPositions.clear();
11511162
}
11521163

11531164
/**
@@ -1223,19 +1234,21 @@ export class DragRef<T = any> {
12231234
}
12241235

12251236
/** Updates the internal state of the draggable element when scrolling has occurred. */
1226-
private _updateOnScroll() {
1227-
const oldScrollPosition = this._scrollPosition;
1228-
const currentScrollPosition = this._viewportRuler.getViewportScrollPosition();
1237+
private _updateOnScroll(event: Event) {
1238+
const scrollDifference = this._parentPositions.handleScroll(event);
12291239

12301240
// ClientRect dimensions are based on the page's scroll position so
12311241
// we have to update the cached boundary ClientRect if the user has scrolled.
1232-
if (oldScrollPosition && this._boundaryRect) {
1233-
const topDifference = oldScrollPosition.top - currentScrollPosition.top;
1234-
const leftDifference = oldScrollPosition.left - currentScrollPosition.left;
1235-
adjustClientRect(this._boundaryRect, topDifference, leftDifference);
1242+
if (this._boundaryRect && scrollDifference) {
1243+
adjustClientRect(this._boundaryRect, scrollDifference.top, scrollDifference.left);
12361244
}
1245+
}
12371246

1238-
this._scrollPosition = currentScrollPosition;
1247+
/** Gets the scroll position of the viewport. */
1248+
private _getViewportScrollPosition() {
1249+
const cachedPosition = this._parentPositions.positions.get(this._document);
1250+
return cachedPosition ? cachedPosition.scrollPosition :
1251+
this._viewportRuler.getViewportScrollPosition();
12391252
}
12401253
}
12411254

0 commit comments

Comments
 (0)