Skip to content

Commit 22967be

Browse files
tomivirkkiclaude
andauthored
fix: use bounding rect check for upload drop zone dragleave detection (#11143)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 81c3007 commit 22967be

File tree

2 files changed

+37
-28
lines changed

2 files changed

+37
-28
lines changed

packages/upload/src/vaadin-upload-drop-zone.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,16 @@ class UploadDropZone extends ElementMixin(ThemableMixin(PolylitMixin(LumoInjecti
191191
/** @private */
192192
__onDragleave(event) {
193193
event.preventDefault();
194-
// Only remove dragover if we're actually leaving the drop zone
195-
// (not just entering a child element)
196-
if (event.relatedTarget && this.contains(event.relatedTarget)) {
194+
// Use bounding rect to detect whether the cursor actually left the drop zone.
195+
// This avoids relying on event.relatedTarget which Safari reports as null
196+
// for elements inside shadow roots, causing false leave detections.
197+
const rect = this.getBoundingClientRect();
198+
if (
199+
event.clientX >= rect.left &&
200+
event.clientX <= rect.right &&
201+
event.clientY >= rect.top &&
202+
event.clientY <= rect.bottom
203+
) {
197204
return;
198205
}
199206
this.__dragover = false;

packages/upload/test/upload-drop-zone.test.ts

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import type { UploadDropZone } from '../src/vaadin-upload-drop-zone.js';
1111
import { UploadManager } from '../src/vaadin-upload-manager.js';
1212
import { createFiles } from './helpers.js';
1313

14-
function createDragEvent(type: string, files: File[] = [], relatedTarget?: EventTarget | null): DragEvent {
14+
function createDragEvent(
15+
type: string,
16+
files: File[] = [],
17+
options?: { clientX?: number; clientY?: number },
18+
): DragEvent {
1519
const dataTransfer = {
1620
files,
1721
items: files.map((file) => ({
@@ -29,8 +33,11 @@ function createDragEvent(type: string, files: File[] = [], relatedTarget?: Event
2933

3034
const event = new Event(type, { bubbles: true, cancelable: true }) as DragEvent;
3135
Object.defineProperty(event, 'dataTransfer', { value: dataTransfer });
32-
if (relatedTarget !== undefined) {
33-
Object.defineProperty(event, 'relatedTarget', { value: relatedTarget });
36+
if (options?.clientX !== undefined) {
37+
Object.defineProperty(event, 'clientX', { value: options.clientX });
38+
}
39+
if (options?.clientY !== undefined) {
40+
Object.defineProperty(event, 'clientY', { value: options.clientY });
3441
}
3542
return event;
3643
}
@@ -142,7 +149,7 @@ describe('vaadin-upload-drop-zone', () => {
142149
await nextFrame();
143150
expect(dropZone.hasAttribute('dragover')).to.be.true;
144151

145-
// Then trigger dragleave
152+
// Then trigger dragleave (coordinates default to 0,0 which is outside the element)
146153
dropZone.dispatchEvent(createDragEvent('dragleave'));
147154
await nextFrame();
148155
expect(dropZone.hasAttribute('dragover')).to.be.false;
@@ -155,46 +162,41 @@ describe('vaadin-upload-drop-zone', () => {
155162
expect(event.defaultPrevented).to.be.true;
156163
});
157164

158-
it('should not remove dragover when dragleave fires for child element', async () => {
159-
// Add a child element to the drop zone
160-
const child = document.createElement('p');
161-
child.textContent = 'Child element';
162-
dropZone.appendChild(child);
163-
await nextFrame();
164-
165+
it('should not remove dragover when cursor is still inside the drop zone', async () => {
165166
// Trigger dragover on drop zone
166167
dropZone.dispatchEvent(createDragEvent('dragover'));
167168
await nextFrame();
168169
expect(dropZone.hasAttribute('dragover')).to.be.true;
169170

170-
// Simulate dragleave when entering child element (relatedTarget is the child)
171-
const dragleaveEvent = createDragEvent('dragleave', [], child);
171+
// Simulate dragleave with cursor coordinates still inside the drop zone
172+
const rect = dropZone.getBoundingClientRect();
173+
const dragleaveEvent = createDragEvent('dragleave', [], {
174+
clientX: rect.left + rect.width / 2,
175+
clientY: rect.top + rect.height / 2,
176+
});
172177
dropZone.dispatchEvent(dragleaveEvent);
173178
await nextFrame();
174179

175-
// Should still have dragover because we're still inside the drop zone
180+
// Should still have dragover because cursor is inside the drop zone
176181
expect(dropZone.hasAttribute('dragover')).to.be.true;
177182
});
178183

179-
it('should only remove dragover when leaving the drop zone entirely', async () => {
180-
// Add a child element
181-
const child = document.createElement('p');
182-
child.textContent = 'Child element';
183-
dropZone.appendChild(child);
184-
await nextFrame();
185-
184+
it('should remove dragover when cursor leaves the drop zone', async () => {
186185
// Trigger dragover
187186
dropZone.dispatchEvent(createDragEvent('dragover'));
188187
await nextFrame();
189188
expect(dropZone.hasAttribute('dragover')).to.be.true;
190189

191-
// Simulate dragleave when leaving the drop zone entirely (relatedTarget is outside)
192-
const outsideElement = document.body;
193-
const dragleaveEvent = createDragEvent('dragleave', [], outsideElement);
190+
// Simulate dragleave with cursor coordinates outside the drop zone
191+
const rect = dropZone.getBoundingClientRect();
192+
const dragleaveEvent = createDragEvent('dragleave', [], {
193+
clientX: rect.right + 10,
194+
clientY: rect.bottom + 10,
195+
});
194196
dropZone.dispatchEvent(dragleaveEvent);
195197
await nextFrame();
196198

197-
// Should remove dragover because we left the drop zone
199+
// Should remove dragover because cursor left the drop zone
198200
expect(dropZone.hasAttribute('dragover')).to.be.false;
199201
});
200202

0 commit comments

Comments
 (0)