Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(drag-n-drop): send two mousemove events to target to make Angular CDK work #34882

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/playwright-core/src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,9 @@ export class Frame extends SdkObject {
// Note: do not perform locator handlers checkpoint to avoid moving the mouse in the middle of a drag operation.
dom.assertDone(await this._retryWithProgressIfNotConnected(progress, target, options.strict, false /* performActionPreChecks */, async handle => {
return handle._retryPointerAction(progress, 'move and up', false, async point => {
// NOTE: Normal browsers emit usually a lot of dragover/mousemove events during drag'n
// drop operations. We want to emit minimal (2) to make Angular CDK work.
await this._page.mouse.move(point.x, point.y);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fishy. Feels like we are working around Angular implementation problem. We could extend the API to allow passing steps, similar to
mouse.move({steps}) and leave the users in control of whether they want intermediate move events, WDYT?

await this._page.mouse.move(point.x, point.y);
await this._page.mouse.up();
}, {
Expand Down
112 changes: 112 additions & 0 deletions tests/assets/drag-n-drop-manual.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<style>
* {
box-sizing: border-box;
}
body, html {
margin: 0;
padding: 0;
}
div:not(.mouse-helper) {
margin: 0;
padding: 0;
}
#source {
color: blue;
border: 1px solid black;
position: absolute;
left: 20px;
top: 20px;
width: 200px;
height: 100px;
cursor: move;
user-select: none;
}
#target {
border: 1px solid black;
position: absolute;
left: 40px;
top: 200px;
width: 400px;
height: 300px;
}
</style>

<body>
<div>
<p id="source">
Select this element, drag it to the Drop Zone and then release the selection to move the element.</p>
</div>
<div id="target">Drop Zone</div>

<script>
const sourceElement = document.getElementById('source');
const targetElement = document.getElementById('target');

let isDragging = false;
let offsetX, offsetY;
let originalPosition = { left: 0, top: 0 };

sourceElement.addEventListener('mousedown', function(e) {
e.preventDefault();

const rect = sourceElement.getBoundingClientRect();
originalPosition = {
left: rect.left,
top: rect.top
};

offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;

isDragging = true;
sourceElement.style.border = 'dashed'
});

document.addEventListener('mousemove', function(e) {
if (!isDragging) return;

sourceElement.style.left = (e.clientX - offsetX) + 'px';
sourceElement.style.top = (e.clientY - offsetY) + 'px';
});

document.addEventListener('mouseup', function(e) {
if (!isDragging) return;

isDragging = false;

const sourceRect = sourceElement.getBoundingClientRect();
const targetRect = targetElement.getBoundingClientRect();

const isOverlapping = !(
sourceRect.right < targetRect.left ||
sourceRect.left > targetRect.right ||
sourceRect.bottom < targetRect.top ||
sourceRect.top > targetRect.bottom
);

if (isOverlapping) {
targetElement.appendChild(sourceElement);

sourceElement.style.removeProperty('position')
sourceElement.style.removeProperty('top')
sourceElement.style.removeProperty('left')

console.log('Drop successful');
} else {
sourceElement.style.left = originalPosition.left + 'px';
sourceElement.style.top = originalPosition.top + 'px';

console.log('Drop failed - returning to original position');
}
});

document.addEventListener('mouseleave', function() {
if (isDragging) {
isDragging = false;

sourceElement.style.left = originalPosition.left + 'px';
sourceElement.style.top = originalPosition.top + 'px';
}
});
</script>
</body>
47 changes: 47 additions & 0 deletions tests/page/page-drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,25 @@ it.describe('Drag and drop', () => {
]);
});

it('should send the right events when using dragTo', async ({ server, page, browserName }) => {
await page.goto(server.PREFIX + '/drag-n-drop.html');
const events = await trackEvents(await page.$('body'));
await page.locator('#source').dragTo(page.locator('#target'));
expect(await events.jsonValue()).toEqual([
'mousemove at 120;86',
'mousedown at 120;86',
browserName === 'firefox' ? 'dragstart at 120;86' : 'mousemove at 240;350',
browserName === 'firefox' ? 'mousemove at 240;350' : 'dragstart at 120;86',
'dragenter at 240;350',
// NOTE: Normal browsers emit usually a lot of dragover events during drag'n
// drop operations. We want to emit minimal (2) to make Angular CDK work.
'dragover at 240;350',
'dragover at 240;350',
'drop at 240;350',
'dragend',
]);
});

it('should not send dragover on the first mousemove', async ({ server, page, browserName }) => {
it.fixme(browserName !== 'chromium');

Expand Down Expand Up @@ -293,6 +312,34 @@ it.describe('Drag and drop', () => {
expect(await page.$eval('#target', target => target.contains(document.querySelector('#source')))).toBe(true); // could not find source in target
});

it('should work with manual drag\'n drop and emit correct mousemove events', {
annotation: {
type: 'issue',
description: 'https://github.com/microsoft/playwright/issues/34688',
}
}, async ({ page, server }) => {
await page.goto(server.PREFIX + '/drag-n-drop-manual.html');
const events = await page.evaluateHandle(() => {
const events = [];
document.addEventListener('mousemove', (event: MouseEvent) => {
events.push({
type: event.type,
x: event.clientX,
y: event.clientY,
});
});
return events;
});
await page.dragAndDrop('#source', '#target');
expect(await events.jsonValue()).toEqual([
{ type: 'mousemove', x: 120, y: 86 },
// NOTE: Normal browsers emit usually a lot of mousemove events during drag'n
// drop operations. We want to emit minimal (2) to make Angular CDK work.
{ type: 'mousemove', x: 240, y: 350 },
{ type: 'mousemove', x: 240, y: 350 },
]);
});

it('should allow specifying the position', async ({ page, server }) => {
await page.setContent(`
<div style="width:100px;height:100px;background:red;" id="red">
Expand Down
Loading