Skip to content

Commit 4495b36

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 a909f81 commit 4495b36

File tree

4 files changed

+175
-75
lines changed

4 files changed

+175
-75
lines changed

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

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
} from '@angular/core';
2424
import {TestBed, ComponentFixture, fakeAsync, flush, tick} from '@angular/core/testing';
2525
import {DOCUMENT} from '@angular/common';
26-
import {ViewportRuler} from '@angular/cdk/scrolling';
26+
import {ViewportRuler, ScrollingModule} from '@angular/cdk/scrolling';
2727
import {_supportsShadowDom} from '@angular/cdk/platform';
2828
import {of as observableOf} from 'rxjs';
2929

@@ -47,7 +47,7 @@ describe('CdkDrag', () => {
4747
extraDeclarations: Type<any>[] = []): ComponentFixture<T> {
4848
TestBed
4949
.configureTestingModule({
50-
imports: [DragDropModule],
50+
imports: [DragDropModule, ScrollingModule],
5151
declarations: [componentType, PassthroughComponent, ...extraDeclarations],
5252
providers: [
5353
{
@@ -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();
@@ -4659,6 +4677,30 @@ class DraggableInScrollableVerticalDropZone extends DraggableInDropZone {
46594677
}
46604678
}
46614679

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

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
AfterContentInit,
2323
} from '@angular/core';
2424
import {Directionality} from '@angular/cdk/bidi';
25+
import {ScrollDispatcher} from '@angular/cdk/scrolling';
2526
import {CdkDrag, CDK_DROP_LIST} from './drag';
2627
import {CdkDragDrop, CdkDragEnter, CdkDragExit, CdkDragSortEvent} from '../drag-events';
2728
import {CdkDropListGroup} from './drop-list-group';
@@ -148,7 +149,13 @@ export class CdkDropList<T = any> implements AfterContentInit, OnDestroy {
148149
/** Element that the drop list is attached to. */
149150
public element: ElementRef<HTMLElement>, dragDrop: DragDrop,
150151
private _changeDetectorRef: ChangeDetectorRef, @Optional() private _dir?: Directionality,
151-
@Optional() @SkipSelf() private _group?: CdkDropListGroup<CdkDropList>) {
152+
@Optional() @SkipSelf() private _group?: CdkDropListGroup<CdkDropList>,
153+
154+
/**
155+
* @deprecated _scrollDispatcher parameter to become required.
156+
* @breaking-change 11.0.0
157+
*/
158+
private _scrollDispatcher?: ScrollDispatcher) {
152159
this._dropListRef = dragDrop.createDropList(element);
153160
this._dropListRef.data = this;
154161
this._dropListRef.enterPredicate = (drag: DragRef<CdkDrag>, drop: DropListRef<CdkDropList>) => {
@@ -165,6 +172,14 @@ export class CdkDropList<T = any> implements AfterContentInit, OnDestroy {
165172
}
166173

167174
ngAfterContentInit() {
175+
// @breaking-change 11.0.0 Remove null check for _scrollDispatcher once it's required.
176+
if (this._scrollDispatcher) {
177+
const scrollableParents = this._scrollDispatcher
178+
.getAncestorScrollContainers(this.element)
179+
.map(scrollable => scrollable.getElementRef().nativeElement);
180+
this._dropListRef.withScrollableParents(scrollableParents);
181+
}
182+
168183
this._draggables.changes
169184
.pipe(startWith(this._draggables), takeUntil(this._destroyed))
170185
.subscribe((items: QueryList<CdkDrag>) => {

0 commit comments

Comments
 (0)