Skip to content

Commit c319431

Browse files
crisbetojelbourn
authored andcommitted
feat(drag-drop): support scrolling parent elements apart from list and viewport (#18082)
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 3cbc94a commit c319431

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
{
@@ -3411,6 +3411,24 @@ describe('CdkDrag', () => {
34113411
cleanup();
34123412
}));
34133413

3414+
it('should be able to auto-scroll a parent container', fakeAsync(() => {
3415+
const fixture = createComponent(DraggableInScrollableParentContainer);
3416+
fixture.detectChanges();
3417+
const item = fixture.componentInstance.dragItems.first.element.nativeElement;
3418+
const container = fixture.nativeElement.querySelector('.container');
3419+
const containerRect = container.getBoundingClientRect();
3420+
3421+
expect(container.scrollTop).toBe(0);
3422+
3423+
startDraggingViaMouse(fixture, item);
3424+
dispatchMouseEvent(document, 'mousemove',
3425+
containerRect.left + containerRect.width / 2, containerRect.top + containerRect.height);
3426+
fixture.detectChanges();
3427+
tickAnimationFrames(20);
3428+
3429+
expect(container.scrollTop).toBeGreaterThan(0);
3430+
}));
3431+
34143432
it('should pick up descendants inside of containers', fakeAsync(() => {
34153433
const fixture = createComponent(DraggableInDropZoneWithContainer);
34163434
fixture.detectChanges();
@@ -4695,6 +4713,30 @@ class DraggableInScrollableVerticalDropZone extends DraggableInDropZone {
46954713
}
46964714
}
46974715

4716+
@Component({
4717+
template: '<div class="container" cdkScrollable>' + DROP_ZONE_FIXTURE_TEMPLATE + '</div>',
4718+
4719+
// Note that it needs a margin to ensure that it's not flush against the viewport
4720+
// edge which will cause the viewport to scroll, rather than the list.
4721+
styles: [`
4722+
.container {
4723+
max-height: 200px;
4724+
overflow: auto;
4725+
margin: 10vw 0 0 10vw;
4726+
}
4727+
`]
4728+
})
4729+
class DraggableInScrollableParentContainer extends DraggableInDropZone {
4730+
constructor() {
4731+
super();
4732+
4733+
for (let i = 0; i < 60; i++) {
4734+
this.items.push({value: `Extra item ${i}`, height: ITEM_HEIGHT, margin: 0});
4735+
}
4736+
}
4737+
}
4738+
4739+
46984740
@Component({
46994741
// Note that we need the blank `ngSwitch` below to hit the code path that we're testing.
47004742
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)