components icon indicating copy to clipboard operation
components copied to clipboard

DragDrop Autoscroll feature not working with Material Table

Open woteska opened this issue 6 years ago • 9 comments

Relates to https://github.com/angular/components/pull/16382

Reproduction

https://stackblitz.com/edit/angular-qczaca

Steps to reproduce:

  1. Try dragging a row to the edge of its container.
  2. You can see that Material Table cannot be autoscrolled while a simple container can be... (like the div in the Stackblitz example).

Expected Behavior

I can use autoscroll feature with Material Table as well.

Actual Behavior

Autoscroll does not work with Material Table.

Environment

Angular, CDK/Material:

    "@angular/animations": "~8.2.0",
    "@angular/cdk": "~8.1.2",
    "@angular/common": "~8.2.0",
    "@angular/compiler": "~8.2.0",
    "@angular/core": "~8.2.0",
    "@angular/forms": "~8.2.0",
    "@angular/material": "^8.1.2",
    "@angular/platform-browser": "~8.2.0",
    "@angular/platform-browser-dynamic": "~8.2.0",
    "@angular/router": "~8.2.0",
    "hammerjs": "^2.0.8"
    "@angular-devkit/build-angular": "~0.802.0",
    "@angular/cli": "~8.2.0",
    "@angular/compiler-cli": "~8.2.0",
    "@angular/language-service": "~8.2.0"

woteska avatar Aug 03 '19 15:08 woteska

@woteska

The problem is the scroll container not being used in the process, instead another element is assumed to be the scroll container.

The cdkDropList will use the element it is defined on as the scroll node, without being able to change it (cdkDrag for example can allow changing the root container)

With your first example

<div cdkDropList class="simple-list">
  <div *ngFor="let item of items" cdkDrag>drag-me-please-{{item}}</div>
</div>

The scroll container is the first DIV which is also the cdkDropList element.

So the drag process uses the proper elements.

In the second example

<div class="example-container mat-elevation-z8">
	<table mat-table [dataSource]="dataSource" cdkDropList>
        </table>
</div>

The scroll container is actually the first DIV and the cdkDropList is the TABLE element.
The drag list will use the TABLE element as the scroll container, however its not.

The TABLE element will render to full height which is 2456px while the DIV will be 200px so actually you never reach the last 5% at the end, except when you fully scrolled to the end which is redundant anyway....


I'v already asked for this feature ages ago because it has other issue it blocks... but the team decided it's not a good thing.

https://github.com/angular/components/issues/14148


BTW, also worth mentioning that the entire placeholder positioning will not work on virtual lists because it's not built for that... the positioning will go all wrong because of it not taking into account dragging of items that are out of view.

shlomiassaf avatar Aug 28 '19 14:08 shlomiassaf

FYI, as a workaround, you can use a "plugin like" directive.

import { Directive, Input, ElementRef } from '@angular/core';
import { CdkDropList } from '@angular/cdk/drag-drop';

@Directive({
  selector: '[cdkDropList][actualContainer]',
})
export class CdkDropListAcualContainer {
  @Input('actualContainer') actualContainer: string;
  originalElement: ElementRef<HTMLElement>;

  constructor(cdkDropList: CdkDropList) {
    cdkDropList._dropListRef.beforeStarted.subscribe( () => {
      if (!this.originalElement) {
        this.originalElement = cdkDropList.element;
      }

      if (this.actualContainer) {
        const element = this.originalElement.nativeElement.closest(this.actualContainer) as HTMLElement;
        cdkDropList.element = new ElementRef<HTMLElement>(element);
        cdkDropList._dropListRef.element = element;
      } else {
        cdkDropList.element = this.originalElement;
        cdkDropList._dropListRef.element = cdkDropList.element.nativeElement;
      }

    });
  }
}

The directive itself might need some polishing for more advanced cases, tear down etc...

<div class="example-container mat-elevation-z8">
	<table mat-table [dataSource]="dataSource" cdkDropList actualContainer="div.example-container">
        </table>
</div>

What happens here is that actualContainer="div.example-container" activates the workaround directive, which search's UP the dom tree for the first element matching the selector provided and define it as the root element for the drag list directive.

Here is a stackblitz example you can use

shlomiassaf avatar Aug 28 '19 15:08 shlomiassaf

I can see that there is an issue in above example. first time you want do drag row in the table beyond the container view (with the scroll), row is not moving correctly. It looks like it's moved only between rows of initial view of the table ( rows that are visible when row is dragged first time). Also param of drop event returns wrong indexes. Second drag and drops works correctly. any Ideas how to solve this ?

SomPatrik avatar Sep 26 '19 09:09 SomPatrik

I can see that there is an issue in above example. first time you want do drag row in the table beyond the container view (with the scroll), row is not moving correctly. It looks like it's moved only between rows of initial view of the table ( rows that are visible when row is dragged first time). Also param of drop event returns wrong indexes. Second drag and drops works correctly. any Ideas how to solve this ?

In the current version, beforeStarted on the cdkDropList declares the element before calling beforeStarted; however, beforeStarted on the cdkDrag calls on time to avoid the issue you mentioned. Adjusting @shlomiassaf's solution, the following worked for me:

Add actualContainer="div.example-container" to the element with cdkDrag instead of cdkDropList and adjust the directive as following:

import { Directive, Input, ElementRef, AfterViewInit } from '@angular/core';
import { CdkDrag } from '@angular/cdk/drag-drop';

@Directive({
  selector: '[cdkDrag][actualContainer]',
})
export class CdkDropListActualContainerDirective {
  @Input('actualContainer') actualContainer: string;
  originalElement: ElementRef<HTMLElement>;

  constructor(cdkDrag: CdkDrag) {
    cdkDrag._dragRef.beforeStarted.subscribe( () => {
      var cdkDropList = cdkDrag.dropContainer;
      if (!this.originalElement) {
        this.originalElement = cdkDropList.element;
      }

      if ( this.actualContainer ) {
        const element = this.originalElement.nativeElement.closest(this.actualContainer) as HTMLElement;
        cdkDropList._dropListRef.element = element;
        cdkDropList.element = new ElementRef<HTMLElement>(element);
      } else {
        cdkDropList._dropListRef.element = cdkDropList.element.nativeElement;
        cdkDropList.element = this.originalElement;
      }
    });
  }
}

kyle-apex avatar Dec 06 '19 15:12 kyle-apex

And like indicated in this issue, it is not just tables. It happens anytime an outside element is scrollable, which is quite often in web apps. You can see it clearly not work with a slightly modified Angular Example from the CDK page by adding a scrollable outside element:

https://stackblitz.com/edit/angular-swaqkk

But it does seem to work with horizontal scroll: https://stackblitz.com/edit/angular-2rzs2i

jraadt avatar Jul 08 '20 20:07 jraadt

This solution(for Angular 9+) helped me for my case. Angular CDK - issue with scrolling and dragging element inside nested scrollable div

Alexander091 avatar Jan 06 '21 17:01 Alexander091

@jraadt, The solution is not well documented, but it works like a charm. Add cdkScrollable to that div with outside classname. Here is the link, tell me if it helped: https://stackblitz.com/edit/angular-swaqkk-jdsxoo?file=src%2Fapp%2Fcdk-drag-drop-sorting-example.html

liesahead avatar Jan 19 '21 13:01 liesahead

Is there a way to make it scroll faster?

rosostolato avatar Jan 17 '23 20:01 rosostolato

Yeah it needs to scroll faster, otherwise while works, it is practically useless.

EDIT: the solution from here

  • https://stackoverflow.com/questions/62671963/angular-cdk-drag-drop-adjust-autoscroll-speed-when-dragging

SUMMARY: use cdkDropListAutoScrollStep, for example [cdkDropListAutoScrollStep]=20.

vzakharov-rxnt avatar Apr 17 '24 11:04 vzakharov-rxnt