Skip to content

Commit 3e1de9d

Browse files
authored
fix(cdk/drag-drop): incorrectly sorting element inside dialog with blocked scrolling (#14806)
We use the `ViewportRuler` to determine how much a page has been scrolled in order to compensate for it in some of the `ClientRect`-related calculations. This breaks down when the drop list is inside a dialog with blocked scrolling, because scroll blocking works by offsetting the `html` node which in turn throws off the `ViewportRuler`. These changes switches to reading the values off the `window` which won't be thrown off. Fixes #14280.
1 parent feaf50b commit 3e1de9d

File tree

4 files changed

+38
-10
lines changed

4 files changed

+38
-10
lines changed

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

+19
Original file line numberDiff line numberDiff line change
@@ -4903,6 +4903,25 @@ describe('CdkDrag', () => {
49034903
.withContext('Expected placeholder to preserve transform when dragging stops.')
49044904
.toBe(true);
49054905
}));
4906+
4907+
it('should sort correctly if the <html> node has been offset', fakeAsync(() => {
4908+
const documentElement = document.documentElement!;
4909+
const fixture = createComponent(DraggableInDropZone);
4910+
fixture.detectChanges();
4911+
4912+
documentElement.style.position = 'absolute';
4913+
documentElement.style.top = '-100px';
4914+
4915+
assertDownwardSorting(
4916+
fixture,
4917+
fixture.componentInstance.dragItems.map(item => {
4918+
return item.element.nativeElement;
4919+
}),
4920+
);
4921+
4922+
documentElement.style.position = '';
4923+
documentElement.style.top = '';
4924+
}));
49064925
});
49074926

49084927
describe('in a connected drop container', () => {

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ export class DragRef<T = any> {
363363
private _dragDropRegistry: DragDropRegistry<DragRef, DropListRef>,
364364
) {
365365
this.withRootElement(element).withParent(_config.parentDragRef || null);
366-
this._parentPositions = new ParentPositionTracker(_document, _viewportRuler);
366+
this._parentPositions = new ParentPositionTracker(_document);
367367
_dragDropRegistry.registerDragItem(this);
368368
}
369369

@@ -1461,10 +1461,10 @@ export class DragRef<T = any> {
14611461

14621462
/** Gets the scroll position of the viewport. */
14631463
private _getViewportScrollPosition() {
1464-
const cachedPosition = this._parentPositions.positions.get(this._document);
1465-
return cachedPosition
1466-
? cachedPosition.scrollPosition
1467-
: this._viewportRuler.getViewportScrollPosition();
1464+
return (
1465+
this._parentPositions.positions.get(this._document)?.scrollPosition ||
1466+
this._parentPositions.getViewportScrollPosition()
1467+
);
14681468
}
14691469

14701470
/**

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ export class DropListRef<T = any> {
230230
this._document = _document;
231231
this.withScrollableParents([this.element]);
232232
_dragDropRegistry.registerDropContainer(this);
233-
this._parentPositions = new ParentPositionTracker(_document, _viewportRuler);
233+
this._parentPositions = new ParentPositionTracker(_document);
234234
}
235235

236236
/** Removes the drop list functionality from the DOM element. */

src/cdk/drag-drop/parent-position-tracker.ts

+13-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {ViewportRuler} from '@angular/cdk/scrolling';
109
import {_getEventTarget} from '@angular/cdk/platform';
1110
import {getMutableClientRect, adjustClientRect} from './client-rect';
1211

@@ -27,7 +26,7 @@ export class ParentPositionTracker {
2726
}
2827
>();
2928

30-
constructor(private _document: Document, private _viewportRuler: ViewportRuler) {}
29+
constructor(private _document: Document) {}
3130

3231
/** Clears the cached positions. */
3332
clear() {
@@ -38,7 +37,7 @@ export class ParentPositionTracker {
3837
cache(elements: readonly HTMLElement[]) {
3938
this.clear();
4039
this.positions.set(this._document, {
41-
scrollPosition: this._viewportRuler.getViewportScrollPosition(),
40+
scrollPosition: this.getViewportScrollPosition(),
4241
});
4342

4443
elements.forEach(element => {
@@ -63,7 +62,7 @@ export class ParentPositionTracker {
6362
let newLeft: number;
6463

6564
if (target === this._document) {
66-
const viewportScrollPosition = this._viewportRuler!.getViewportScrollPosition();
65+
const viewportScrollPosition = this.getViewportScrollPosition();
6766
newTop = viewportScrollPosition.top;
6867
newLeft = viewportScrollPosition.left;
6968
} else {
@@ -87,4 +86,14 @@ export class ParentPositionTracker {
8786

8887
return {top: topDifference, left: leftDifference};
8988
}
89+
90+
/**
91+
* Gets the scroll position of the viewport. Note that we use the scrollX and scrollY directly,
92+
* instead of going through the `ViewportRuler`, because the first value the ruler looks at is
93+
* the top/left offset of the `document.documentElement` which works for most cases, but breaks
94+
* if the element is offset by something like the `BlockScrollStrategy`.
95+
*/
96+
getViewportScrollPosition() {
97+
return {top: window.scrollY, left: window.scrollX};
98+
}
9099
}

0 commit comments

Comments
 (0)