cdkDrag: with cdkDropList -> wrong Cursor when dragging
Hi all,

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>
Is this reporting an issue with the example or it's CSS? or is this reporting an issue with the cdkDrag directive?
I think its a issue with the cdkDrag directive. Before version 7.2.x it works fine
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.
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.
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 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 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;
}
@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...
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 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 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.
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 :)
I have fixed it with this custom directive. Just use it for the same element where the
cdkDragdirective 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
I have fixed it with this custom directive. Just use it for the same element where the
cdkDragdirective 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;
}
});