Skip to content

Commit e62fa20

Browse files
committed
feat(drag-drop): support scrolling parent elements apart from list and viewport
Currently for performance reasons we only support scrolling within the drop list itself or the viewport, however in some cases the scrollable container might be different. These changes add a new input that consumers can use to tell the CDK which other parents can be scrolled. Fixes #18072. Relates to #13588.
1 parent 09dc459 commit e62fa20

File tree

6 files changed

+213
-90
lines changed

6 files changed

+213
-90
lines changed

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3375,6 +3375,24 @@ describe('CdkDrag', () => {
33753375
cleanup();
33763376
}));
33773377

3378+
it('should be able to auto-scroll a parent container', fakeAsync(() => {
3379+
const fixture = createComponent(DraggableInScrollableParentContainer);
3380+
fixture.detectChanges();
3381+
const item = fixture.componentInstance.dragItems.first.element.nativeElement;
3382+
const container = fixture.nativeElement.querySelector('.container');
3383+
const containerRect = container.getBoundingClientRect();
3384+
3385+
expect(container.scrollTop).toBe(0);
3386+
3387+
startDraggingViaMouse(fixture, item);
3388+
dispatchMouseEvent(document, 'mousemove',
3389+
containerRect.left + containerRect.width / 2, containerRect.top + containerRect.height);
3390+
fixture.detectChanges();
3391+
tickAnimationFrames(20);
3392+
3393+
expect(container.scrollTop).toBeGreaterThan(0);
3394+
}));
3395+
33783396
it('should pick up descendants inside of containers', fakeAsync(() => {
33793397
const fixture = createComponent(DraggableInDropZoneWithContainer);
33803398
fixture.detectChanges();
@@ -4596,6 +4614,7 @@ const DROP_ZONE_FIXTURE_TEMPLATE = `
45964614
style="width: 100px; background: pink;"
45974615
[id]="dropZoneId"
45984616
[cdkDropListData]="items"
4617+
[cdkDropListScrollableParents]="scrollableParentsSelector"
45994618
(cdkDropListSorted)="sortedSpy($event)"
46004619
(cdkDropListDropped)="droppedSpy($event)">
46014620
<div
@@ -4621,6 +4640,7 @@ class DraggableInDropZone {
46214640
{value: 'Three', height: ITEM_HEIGHT, margin: 0}
46224641
];
46234642
dropZoneId = 'items';
4643+
scrollableParentsSelector: string;
46244644
boundarySelector: string;
46254645
previewClass: string | string[];
46264646
sortedSpy = jasmine.createSpy('sorted spy');
@@ -4659,6 +4679,31 @@ class DraggableInScrollableVerticalDropZone extends DraggableInDropZone {
46594679
}
46604680
}
46614681

4682+
@Component({
4683+
template: '<div class="container">' + DROP_ZONE_FIXTURE_TEMPLATE + '</div>',
4684+
4685+
// Note that it needs a margin to ensure that it's not flush against the viewport
4686+
// edge which will cause the viewport to scroll, rather than the list.
4687+
styles: [`
4688+
.container {
4689+
max-height: 200px;
4690+
overflow: auto;
4691+
margin: 10vw 0 0 10vw;
4692+
}
4693+
`]
4694+
})
4695+
class DraggableInScrollableParentContainer extends DraggableInDropZone {
4696+
constructor() {
4697+
super();
4698+
this.scrollableParentsSelector = '.container';
4699+
4700+
for (let i = 0; i < 60; i++) {
4701+
this.items.push({value: `Extra item ${i}`, height: ITEM_HEIGHT, margin: 0});
4702+
}
4703+
}
4704+
}
4705+
4706+
46624707
@Component({
46634708
// Note that we need the blank `ngSwitch` below to hit the code path that we're testing.
46644709
template: `

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

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import {CDK_DRAG_PARENT} from '../drag-parent';
5454
import {DragRef, DragRefConfig, Point} from '../drag-ref';
5555
import {CdkDropListInternal as CdkDropList} from './drop-list';
5656
import {DragDrop} from '../drag-drop';
57+
import {getClosestMatchingAncestor} from './util';
5758

5859
/**
5960
* Injection token that is used to provide a CdkDropList instance to CdkDrag.
@@ -416,21 +417,3 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
416417

417418
static ngAcceptInputType_disabled: BooleanInput;
418419
}
419-
420-
/** Gets the closest ancestor of an element that matches a selector. */
421-
function getClosestMatchingAncestor(element: HTMLElement, selector: string) {
422-
let currentElement = element.parentElement as HTMLElement | null;
423-
424-
while (currentElement) {
425-
// IE doesn't support `matches` so we have to fall back to `msMatchesSelector`.
426-
if (currentElement.matches ? currentElement.matches(selector) :
427-
(currentElement as any).msMatchesSelector(selector)) {
428-
return currentElement;
429-
}
430-
431-
currentElement = currentElement.parentElement;
432-
}
433-
434-
return null;
435-
}
436-

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {CdkDropListGroup} from './drop-list-group';
2828
import {DropListRef} from '../drop-list-ref';
2929
import {DragRef} from '../drag-ref';
3030
import {DragDrop} from '../drag-drop';
31+
import {getClosestMatchingAncestor} from './util';
3132
import {Subject} from 'rxjs';
3233
import {startWith, takeUntil} from 'rxjs/operators';
3334

@@ -123,6 +124,15 @@ export class CdkDropList<T = any> implements AfterContentInit, OnDestroy {
123124
@Input('cdkDropListAutoScrollDisabled')
124125
autoScrollDisabled: boolean = false;
125126

127+
/**
128+
* CSS selector that will be used to determine which parent elements can be scrolled while the
129+
* user is dragging an item in the list. The viewport and the list itself are always allowed
130+
* to be scrollable, but for performance reasons scrolling won't be monitored on any other
131+
* parent elements that don't match this selector.
132+
*/
133+
@Input('cdkDropListScrollableParents')
134+
scrollableParents: string;
135+
126136
/** Emits when the user drops an item inside the container. */
127137
@Output('cdkDropListDropped')
128138
dropped: EventEmitter<CdkDragDrop<T, any>> = new EventEmitter<CdkDragDrop<T, any>>();
@@ -165,6 +175,11 @@ export class CdkDropList<T = any> implements AfterContentInit, OnDestroy {
165175
}
166176

167177
ngAfterContentInit() {
178+
if (this.scrollableParents) {
179+
this._dropListRef.withScrollableParents(
180+
this._resolveScrollableParents(this.scrollableParents));
181+
}
182+
168183
this._draggables.changes
169184
.pipe(startWith(this._draggables), takeUntil(this._destroyed))
170185
.subscribe((items: QueryList<CdkDrag>) => {
@@ -332,6 +347,19 @@ export class CdkDropList<T = any> implements AfterContentInit, OnDestroy {
332347
});
333348
}
334349

350+
/** Resolves the scrollable parent elements by matching them against a selector. */
351+
private _resolveScrollableParents(selector: string): HTMLElement[] {
352+
const elements: HTMLElement[] = [];
353+
let parent = getClosestMatchingAncestor(this.element.nativeElement, selector);
354+
355+
while (parent) {
356+
elements.push(parent);
357+
parent = getClosestMatchingAncestor(parent, selector);
358+
}
359+
360+
return elements;
361+
}
362+
335363
static ngAcceptInputType_disabled: BooleanInput;
336364
static ngAcceptInputType_sortingDisabled: BooleanInput;
337365
static ngAcceptInputType_autoScrollDisabled: BooleanInput;

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
/** Gets the closest ancestor of an element that matches a selector. */
10+
export function getClosestMatchingAncestor(element: HTMLElement, selector: string) {
11+
let currentElement = element.parentElement as HTMLElement | null;
12+
13+
while (currentElement) {
14+
// IE doesn't support `matches` so we have to fall back to `msMatchesSelector`.
15+
if (currentElement.matches ? currentElement.matches(selector) :
16+
(currentElement as any).msMatchesSelector(selector)) {
17+
return currentElement;
18+
}
19+
20+
currentElement = currentElement.parentElement;
21+
}
22+
23+
return null;
24+
}

0 commit comments

Comments
 (0)