components icon indicating copy to clipboard operation
components copied to clipboard

cdkDrag: with cdkDropList -> wrong Cursor when dragging

Open Flo0806 opened this issue 5 years ago • 14 comments

Hi all, issue_1 issue_2

Here is a little sample. https://stackblitz.com/angular/yvmnrxexppp?file=app%2Fcdk-drag-drop-connected-sorting-example.html

Since version 7.2.x (I noticed) there is the problem that the cursor (set in CSS) adapts to the underlying object during the dragging process.

If the cursor is changed to "move" and I use e.g. move an "input" (drag) then the cursor changes from move to input.

This phenomenon only occurs with cdkDropList, not when the cdkDrag property is only set for a single object.

The CSS:

.example-box {
  padding: 20px 10px;
  border-bottom: solid 1px #ccc;
  color: rgba(0, 0, 0, 0.87);

  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  box-sizing: border-box;
  cursor: move;
  background: white;
  font-size: 14px;
}

The HTML:

   <div class="col-md-4">

      <div class="drag-container">
        <div class="section-heading">Still Doing</div>
        <div cdkDropList #pendingList="cdkDropList" [cdkDropListData]="todo"
          [cdkDropListConnectedTo]="[doneList,reviewList]" class="item-list" (cdkDropListDropped)="drop($event)">
          <div class="item-box" *ngFor="let item of todo" cdkDrag>{{item}}</div>
        </div>
      </div>
    </div>

Flo0806 avatar Jul 24 '20 13:07 Flo0806

Is this reporting an issue with the example or it's CSS? or is this reporting an issue with the cdkDrag directive?

Splaktar avatar Aug 07 '20 18:08 Splaktar

I think its a issue with the cdkDrag directive. Before version 7.2.x it works fine

Flo0806 avatar Aug 08 '20 09:08 Flo0806

Please submit Angular Material and CDK questions here and issues here. This repo is for docs infrastructure only.

I have transferred this issue to the correct repository for you.

Splaktar avatar Aug 08 '20 09:08 Splaktar

The way we detect whether the user's pointer is over a drop list is through document.elementFromPoint, but the problem is that the dragged element will always be under the user's pointer. We've worked around it by setting pointer-events: none on the element while it's dragging which is why your cursor goes away.

It could be worked around by using document.elementsFromPoint and skipping the dragged item's preview element while going through the array of results, but we'd have to account for some browser differences and do some testing to ensure that performance doesn't degrade since the latter method is doing a bit more work.

crisbeto avatar Aug 09 '20 08:08 crisbeto

While implementing custom drag and drop directive (before Angular CDK) i was able to resolve this by hiding/showing dragImage i in a single atomic cycle while mouse moves

  dragging(ev: MouseEvent) {
    // Get element behind drag image at following mouse coordinates
    dragImage.style.visibility = "hidden";
    const curHoveredElement = document.elementFromPoint(ev.clientX, ev.clientY);
    dragImage.style.visibility = "visible";
  }

Did not experience flickering/drawing problems so far (although it was tested on not big drag images/preview objects)

marijanlekic avatar Dec 14 '20 23:12 marijanlekic

@marijanlekic can you provide a complete example with html? EG what drag event are you listening to? What is the point of accessing the curHoveredElement?

Also @crisbeto you mention in a couple places, https://github.com/angular/components/issues/15090#issuecomment-462449477, that this can be worked around with current APIs. An example (stackblitz would be awesome) of working around this with current APIs would go a long way.

newmanw avatar Jan 16 '21 18:01 newmanw

@newmanw what I was referring to in that comment is that you can add a class to the body when dragging starts using the cdkDragStarted class and then remove it when it stops on cdkDragEnded. Then you can apply the cursor style to that global class. E.g.

body.is-dragging {
  cursor: move;
}

crisbeto avatar Jan 17 '21 13:01 crisbeto

@newmanw

I wrote my own drag and drop functionality so i dont use CDK drag events. In the case that i wrote in my prev comment dragging is being called on event that is similar to dragMove (between drag start and drag end). The point of curHoveredElement in my case is to trigger dragEnter, dragLeave, dragOver (or such custom events) on the element that is below dragging preview element.

By using "pointer-events:none" we are not not able to control cursor in the way that we want to (wrong cursor/getting cursor from the element that is below dragging preview element), and also, when we use pointerEvents: none, the element that is below dragging preview element, will get events such as "mouseenter", "mouseleave", "mousemove", etc.

So basically i resolved this by not setting "pointer-events:none". Instead, i set cursor to anything that i want to and i get element that is below dragging image by hiding preview element and showing it.

    previewImage.style.visibility = "hidden";
    // Get element below preview element
    const elementBelow = document.elementFromPoint(ev.clientX, ev.clientY);
    previewImage.style.visibility = "visible";
    // Trigger events on elementBelow (dragEnter, dragOver, dragLeave...)

For my needs, i did not face any rendering/other problems...

marijanlekic avatar Jan 18 '21 00:01 marijanlekic

I have fixed it with this custom directive. Just use it for the same element where the cdkDrag directive is hosted:

@Directive({
  selector: '[appDragCursor]',
})
export class DragCursorDirective implements OnInit, OnDestroy {
  private unsubscribe$: Subject<void> = new Subject();

  constructor(@Inject(WINDOW) private window: Window, private cdkDrag: CdkDrag) {}

  public ngOnInit(): void {
    this.cdkDrag.started.pipe(takeUntil(this.unsubscribe$)).subscribe(() => {
      this.window.document.body.style.cursor = 'move';
    });

    this.cdkDrag.ended.pipe(takeUntil(this.unsubscribe$)).subscribe(() => {
      this.window.document.body.style.cursor = 'auto';
    });
  }

  public ngOnDestroy(): void {
    this.unsubscribe$.next();
    this.unsubscribe$.complete();
  }
}

ohryk-intellias avatar Feb 02 '21 11:02 ohryk-intellias

For myself, I added the following style override to re-enable the custom cursor while dragging.

.draggable-element .drag-handle{
  cursor: grab;
}

.draggable-element.cdk-drag-preview .drag-handle{
  pointer-events: auto;
  cursor: grabbing;
}

Live Example: https://stackblitz.com/edit/angular-g2yu5w?file=src/app/cdk-drag-drop-custom-placeholder-example.css

joelkesler avatar Sep 28 '21 04:09 joelkesler

@joelkesler how are you able to drop the element now?

pointer-events: auto;

But im not able to drop my element if the style is not pointer-events: none;. Propably the CDK "Overlay" blocks the event propagation.

Sebastian-G avatar Mar 16 '22 10:03 Sebastian-G

I'm facing the same issue. The solution presented by @joelkesler works but there's some latency to enable to drop in the target drop list. After trying @ohryk-intellias sugestion it works like a charm! Thanks for that! But IMHO I think this should be supported by default :)

jgomesmv avatar Mar 24 '22 15:03 jgomesmv

I have fixed it with this custom directive. Just use it for the same element where the cdkDrag directive is hosted:

@Directive({
  selector: '[appDragCursor]',
})
export class DragCursorDirective implements OnInit, OnDestroy {
  private unsubscribe$: Subject<void> = new Subject();

  constructor(@Inject(WINDOW) private window: Window, private cdkDrag: CdkDrag) {}

  public ngOnInit(): void {
    this.cdkDrag.started.pipe(takeUntil(this.unsubscribe$)).subscribe(() => {
      this.window.document.body.style.cursor = 'move';
    });

    this.cdkDrag.ended.pipe(takeUntil(this.unsubscribe$)).subscribe(() => {
      this.window.document.body.style.cursor = 'auto';
    });
  }

  public ngOnDestroy(): void {
    this.unsubscribe$.next();
    this.unsubscribe$.complete();
  }
}

For anyone that cannot apply this solution directly because of error TS2552: Cannot find name 'WINDOW'. Did you mean 'Window'? I fixed it in my case by applying https://stackoverflow.com/a/58936690/19957183

Davidihl avatar Oct 02 '24 09:10 Davidihl

I have fixed it with this custom directive. Just use it for the same element where the cdkDrag directive is hosted:

@Directive({
  selector: '[appDragCursor]',
})
export class DragCursorDirective implements OnInit, OnDestroy {
  private unsubscribe$: Subject<void> = new Subject();

  constructor(@Inject(WINDOW) private window: Window, private cdkDrag: CdkDrag) {}

  public ngOnInit(): void {
    this.cdkDrag.started.pipe(takeUntil(this.unsubscribe$)).subscribe(() => {
      this.window.document.body.style.cursor = 'move';
    });

    this.cdkDrag.ended.pipe(takeUntil(this.unsubscribe$)).subscribe(() => {
      this.window.document.body.style.cursor = 'auto';
    });
  }

  public ngOnDestroy(): void {
    this.unsubscribe$.next();
    this.unsubscribe$.complete();
  }
}

For anyone that cannot apply this solution directly because of error TS2552: Cannot find name 'WINDOW'. Did you mean 'Window'? I fixed it in my case by applying https://stackoverflow.com/a/58936690/19957183

Hi, the WINDOW is the custom injection token. Something like this:

import { DOCUMENT } from '@angular/common';
import { inject, InjectionToken } from '@angular/core';

const WINDOW = new InjectionToken<Window>('Window', {
  factory: () => {
    const document = inject(DOCUMENT);
    return document.defaultView;
  }
});

ohryk-intellias avatar Oct 02 '24 09:10 ohryk-intellias