Skip to content

Commit 792e886

Browse files
crisbetojelbourn
authored andcommitted
fix(drag-drop): handle list and viewport auto scroll regions overlapping (#16675)
The way auto-scrolling is currently set up is that we have regions that are X% of the width/height of a node and whenever the user's pointer gets into the region, we start scrolling either the element or the page. The problem with our current approach is that as soon we find that the user's pointer is in one node's scroll region, we disregard any of the other scroll regions and we start scrolling, even if the element can't actually scroll. This is problematic in cases where the scroll list might be close to the viewport's edge and thus it could have its region overlapping with the viewport's. These changes address the issue in two ways: 1. We have the list's scroll regions take precedence over the ones of the page. 2. If the regions overlap, we check whether the element can continue scrolling that direction and if it can't, we start scrolling the page. Fixes #16647.
1 parent e9baa09 commit 792e886

File tree

2 files changed

+101
-19
lines changed

2 files changed

+101
-19
lines changed

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

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3081,9 +3081,47 @@ describe('CdkDrag', () => {
30813081
cleanup();
30823082
}));
30833083

3084-
it('should auto-scroll the viewport, not the list, when the pointer is over the edge of ' +
3084+
it('should auto-scroll the list, not the viewport, when the pointer is over the edge of ' +
30853085
'both the list and the viewport', fakeAsync(() => {
3086-
const fixture = createComponent(DraggableInDropZone);
3086+
const fixture = createComponent(DraggableInScrollableVerticalDropZone);
3087+
fixture.detectChanges();
3088+
3089+
const list = fixture.componentInstance.dropInstance.element.nativeElement;
3090+
const viewportRuler: ViewportRuler = TestBed.get(ViewportRuler);
3091+
const item = fixture.componentInstance.dragItems.first.element.nativeElement;
3092+
3093+
// Position the list so that its top aligns with the viewport top. That way the pointer
3094+
// will both over its top edge and the viewport's. We use top instead of bottom, because
3095+
// bottom behaves weirdly when we run tests on mobile devices.
3096+
list.style.position = 'fixed';
3097+
list.style.left = '50%';
3098+
list.style.top = '0';
3099+
list.style.margin = '0';
3100+
3101+
const listRect = list.getBoundingClientRect();
3102+
const cleanup = makePageScrollable();
3103+
3104+
scrollTo(0, viewportRuler.getViewportSize().height * 5);
3105+
list.scrollTop = 50;
3106+
3107+
const initialScrollDistance = viewportRuler.getViewportScrollPosition().top;
3108+
expect(initialScrollDistance).toBeGreaterThan(0);
3109+
expect(list.scrollTop).toBe(50);
3110+
3111+
startDraggingViaMouse(fixture, item);
3112+
dispatchMouseEvent(document, 'mousemove', listRect.left + listRect.width / 2, 0);
3113+
fixture.detectChanges();
3114+
tickAnimationFrames(20);
3115+
3116+
expect(viewportRuler.getViewportScrollPosition().top).toBe(initialScrollDistance);
3117+
expect(list.scrollTop).toBeLessThan(50);
3118+
3119+
cleanup();
3120+
}));
3121+
3122+
it('should auto-scroll the viewport, when the pointer is over the edge of both the list ' +
3123+
'and the viewport, if the list cannot be scrolled in that direction', fakeAsync(() => {
3124+
const fixture = createComponent(DraggableInScrollableVerticalDropZone);
30873125
fixture.detectChanges();
30883126

30893127
const list = fixture.componentInstance.dropInstance.element.nativeElement;
@@ -3102,6 +3140,7 @@ describe('CdkDrag', () => {
31023140
const cleanup = makePageScrollable();
31033141

31043142
scrollTo(0, viewportRuler.getViewportSize().height * 5);
3143+
list.scrollTop = 0;
31053144

31063145
const initialScrollDistance = viewportRuler.getViewportScrollPosition().top;
31073146
expect(initialScrollDistance).toBeGreaterThan(0);

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

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -510,31 +510,28 @@ export class DropListRef<T = any> {
510510
let verticalScrollDirection = AutoScrollVerticalDirection.NONE;
511511
let horizontalScrollDirection = AutoScrollHorizontalDirection.NONE;
512512

513+
// Check whether we should start scrolling the container.
514+
if (this._isPointerNearDropContainer(pointerX, pointerY)) {
515+
const element = coerceElement(this.element);
516+
517+
[verticalScrollDirection, horizontalScrollDirection] =
518+
getElementScrollDirections(element, this._clientRect, pointerX, pointerY);
519+
520+
if (verticalScrollDirection || horizontalScrollDirection) {
521+
scrollNode = element;
522+
}
523+
}
524+
513525
// @breaking-change 9.0.0 Remove null check for _viewportRuler once it's a required parameter.
514-
// Check whether we're in range to scroll the viewport.
515-
if (this._viewportRuler) {
526+
// Otherwise check if we can start scrolling the viewport.
527+
if (this._viewportRuler && !verticalScrollDirection && !horizontalScrollDirection) {
516528
const {width, height} = this._viewportRuler.getViewportSize();
517529
const clientRect = {width, height, top: 0, right: width, bottom: height, left: 0};
518530
verticalScrollDirection = getVerticalScrollDirection(clientRect, pointerY);
519531
horizontalScrollDirection = getHorizontalScrollDirection(clientRect, pointerX);
520532
scrollNode = window;
521533
}
522534

523-
// If we couldn't find a scroll direction based on the
524-
// window, try with the container, if the pointer is close by.
525-
if (!verticalScrollDirection && !horizontalScrollDirection &&
526-
this._isPointerNearDropContainer(pointerX, pointerY)) {
527-
verticalScrollDirection = getVerticalScrollDirection(this._clientRect, pointerY);
528-
horizontalScrollDirection = getHorizontalScrollDirection(this._clientRect, pointerX);
529-
scrollNode = coerceElement(this.element);
530-
}
531-
532-
// TODO(crisbeto): we also need to account for whether the view or element are scrollable in
533-
// the first place. With the current approach we'll still try to scroll them, but it just
534-
// won't do anything. The only case where this is relevant is that if we have a scrollable
535-
// list close to the viewport edge where the viewport isn't scrollable. In this case the
536-
// we'll be trying to scroll the viewport rather than the list.
537-
538535
if (scrollNode && (verticalScrollDirection !== this._verticalScrollDirection ||
539536
horizontalScrollDirection !== this._horizontalScrollDirection ||
540537
scrollNode !== this._scrollNode)) {
@@ -1006,3 +1003,49 @@ function getHorizontalScrollDirection(clientRect: ClientRect, pointerX: number)
10061003

10071004
return AutoScrollHorizontalDirection.NONE;
10081005
}
1006+
1007+
/**
1008+
* Gets the directions in which an element node should be scrolled,
1009+
* assuming that the user's pointer is already within it scrollable region.
1010+
* @param element Element for which we should calculate the scroll direction.
1011+
* @param clientRect Bounding client rectangle of the element.
1012+
* @param pointerX Position of the user's pointer along the x axis.
1013+
* @param pointerY Position of the user's pointer along the y axis.
1014+
*/
1015+
function getElementScrollDirections(element: HTMLElement, clientRect: ClientRect, pointerX: number,
1016+
pointerY: number): [AutoScrollVerticalDirection, AutoScrollHorizontalDirection] {
1017+
const computedVertical = getVerticalScrollDirection(clientRect, pointerY);
1018+
const computedHorizontal = getHorizontalScrollDirection(clientRect, pointerX);
1019+
let verticalScrollDirection = AutoScrollVerticalDirection.NONE;
1020+
let horizontalScrollDirection = AutoScrollHorizontalDirection.NONE;
1021+
1022+
// Note that we here we do some extra checks for whether the element is actually scrollable in
1023+
// a certain direction and we only assign the scroll direction if it is. We do this so that we
1024+
// can allow other elements to be scrolled, if the current element can't be scrolled anymore.
1025+
// This allows us to handle cases where the scroll regions of two scrollable elements overlap.
1026+
if (computedVertical) {
1027+
const scrollTop = element.scrollTop;
1028+
1029+
if (computedVertical === AutoScrollVerticalDirection.UP) {
1030+
if (scrollTop > 0) {
1031+
verticalScrollDirection = AutoScrollVerticalDirection.UP;
1032+
}
1033+
} else if (element.scrollHeight - scrollTop > element.clientHeight) {
1034+
verticalScrollDirection = AutoScrollVerticalDirection.DOWN;
1035+
}
1036+
}
1037+
1038+
if (computedHorizontal) {
1039+
const scrollLeft = element.scrollLeft;
1040+
1041+
if (computedHorizontal === AutoScrollHorizontalDirection.LEFT) {
1042+
if (scrollLeft > 0) {
1043+
horizontalScrollDirection = AutoScrollHorizontalDirection.LEFT;
1044+
}
1045+
} else if (element.scrollWidth - scrollLeft > element.clientWidth) {
1046+
horizontalScrollDirection = AutoScrollHorizontalDirection.RIGHT;
1047+
}
1048+
}
1049+
1050+
return [verticalScrollDirection, horizontalScrollDirection];
1051+
}

0 commit comments

Comments
 (0)