Skip to content

Commit

Permalink
feat(drag-drop): support scrolling parent elements apart from list an…
Browse files Browse the repository at this point in the history
…d 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.
  • Loading branch information
crisbeto committed Jan 1, 2020
1 parent 09dc459 commit e62fa20
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 90 deletions.
45 changes: 45 additions & 0 deletions src/cdk/drag-drop/directives/drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3375,6 +3375,24 @@ describe('CdkDrag', () => {
cleanup();
}));

it('should be able to auto-scroll a parent container', fakeAsync(() => {
const fixture = createComponent(DraggableInScrollableParentContainer);
fixture.detectChanges();
const item = fixture.componentInstance.dragItems.first.element.nativeElement;
const container = fixture.nativeElement.querySelector('.container');
const containerRect = container.getBoundingClientRect();

expect(container.scrollTop).toBe(0);

startDraggingViaMouse(fixture, item);
dispatchMouseEvent(document, 'mousemove',
containerRect.left + containerRect.width / 2, containerRect.top + containerRect.height);
fixture.detectChanges();
tickAnimationFrames(20);

expect(container.scrollTop).toBeGreaterThan(0);
}));

it('should pick up descendants inside of containers', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZoneWithContainer);
fixture.detectChanges();
Expand Down Expand Up @@ -4596,6 +4614,7 @@ const DROP_ZONE_FIXTURE_TEMPLATE = `
style="width: 100px; background: pink;"
[id]="dropZoneId"
[cdkDropListData]="items"
[cdkDropListScrollableParents]="scrollableParentsSelector"
(cdkDropListSorted)="sortedSpy($event)"
(cdkDropListDropped)="droppedSpy($event)">
<div
Expand All @@ -4621,6 +4640,7 @@ class DraggableInDropZone {
{value: 'Three', height: ITEM_HEIGHT, margin: 0}
];
dropZoneId = 'items';
scrollableParentsSelector: string;
boundarySelector: string;
previewClass: string | string[];
sortedSpy = jasmine.createSpy('sorted spy');
Expand Down Expand Up @@ -4659,6 +4679,31 @@ class DraggableInScrollableVerticalDropZone extends DraggableInDropZone {
}
}

@Component({
template: '<div class="container">' + DROP_ZONE_FIXTURE_TEMPLATE + '</div>',

// Note that it needs a margin to ensure that it's not flush against the viewport
// edge which will cause the viewport to scroll, rather than the list.
styles: [`
.container {
max-height: 200px;
overflow: auto;
margin: 10vw 0 0 10vw;
}
`]
})
class DraggableInScrollableParentContainer extends DraggableInDropZone {
constructor() {
super();
this.scrollableParentsSelector = '.container';

for (let i = 0; i < 60; i++) {
this.items.push({value: `Extra item ${i}`, height: ITEM_HEIGHT, margin: 0});
}
}
}


@Component({
// Note that we need the blank `ngSwitch` below to hit the code path that we're testing.
template: `
Expand Down
19 changes: 1 addition & 18 deletions src/cdk/drag-drop/directives/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {CDK_DRAG_PARENT} from '../drag-parent';
import {DragRef, DragRefConfig, Point} from '../drag-ref';
import {CdkDropListInternal as CdkDropList} from './drop-list';
import {DragDrop} from '../drag-drop';
import {getClosestMatchingAncestor} from './util';

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

static ngAcceptInputType_disabled: BooleanInput;
}

/** Gets the closest ancestor of an element that matches a selector. */
function getClosestMatchingAncestor(element: HTMLElement, selector: string) {
let currentElement = element.parentElement as HTMLElement | null;

while (currentElement) {
// IE doesn't support `matches` so we have to fall back to `msMatchesSelector`.
if (currentElement.matches ? currentElement.matches(selector) :
(currentElement as any).msMatchesSelector(selector)) {
return currentElement;
}

currentElement = currentElement.parentElement;
}

return null;
}

28 changes: 28 additions & 0 deletions src/cdk/drag-drop/directives/drop-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {CdkDropListGroup} from './drop-list-group';
import {DropListRef} from '../drop-list-ref';
import {DragRef} from '../drag-ref';
import {DragDrop} from '../drag-drop';
import {getClosestMatchingAncestor} from './util';
import {Subject} from 'rxjs';
import {startWith, takeUntil} from 'rxjs/operators';

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

/**
* CSS selector that will be used to determine which parent elements can be scrolled while the
* user is dragging an item in the list. The viewport and the list itself are always allowed
* to be scrollable, but for performance reasons scrolling won't be monitored on any other
* parent elements that don't match this selector.
*/
@Input('cdkDropListScrollableParents')
scrollableParents: string;

/** Emits when the user drops an item inside the container. */
@Output('cdkDropListDropped')
dropped: EventEmitter<CdkDragDrop<T, any>> = new EventEmitter<CdkDragDrop<T, any>>();
Expand Down Expand Up @@ -165,6 +175,11 @@ export class CdkDropList<T = any> implements AfterContentInit, OnDestroy {
}

ngAfterContentInit() {
if (this.scrollableParents) {
this._dropListRef.withScrollableParents(
this._resolveScrollableParents(this.scrollableParents));
}

this._draggables.changes
.pipe(startWith(this._draggables), takeUntil(this._destroyed))
.subscribe((items: QueryList<CdkDrag>) => {
Expand Down Expand Up @@ -332,6 +347,19 @@ export class CdkDropList<T = any> implements AfterContentInit, OnDestroy {
});
}

/** Resolves the scrollable parent elements by matching them against a selector. */
private _resolveScrollableParents(selector: string): HTMLElement[] {
const elements: HTMLElement[] = [];
let parent = getClosestMatchingAncestor(this.element.nativeElement, selector);

while (parent) {
elements.push(parent);
parent = getClosestMatchingAncestor(parent, selector);
}

return elements;
}

static ngAcceptInputType_disabled: BooleanInput;
static ngAcceptInputType_sortingDisabled: BooleanInput;
static ngAcceptInputType_autoScrollDisabled: BooleanInput;
Expand Down
24 changes: 24 additions & 0 deletions src/cdk/drag-drop/directives/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

/** Gets the closest ancestor of an element that matches a selector. */
export function getClosestMatchingAncestor(element: HTMLElement, selector: string) {
let currentElement = element.parentElement as HTMLElement | null;

while (currentElement) {
// IE doesn't support `matches` so we have to fall back to `msMatchesSelector`.
if (currentElement.matches ? currentElement.matches(selector) :
(currentElement as any).msMatchesSelector(selector)) {
return currentElement;
}

currentElement = currentElement.parentElement;
}

return null;
}
Loading

0 comments on commit e62fa20

Please sign in to comment.