Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
45 changes: 44 additions & 1 deletion apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1381,7 +1381,7 @@
}
});
return () => setKmlFileImportHandler(null);
}, [addImportedVectorLayers, confirmLargeVectorDataset, t]);

Check warning on line 1384 in apps/geolibre-desktop/src/components/layout/DesktopShell.tsx

View workflow job for this annotation

GitHub Actions / Build and test

React Hook useEffect has an unnecessary dependency: 'confirmLargeVectorDataset'. Either exclude it or remove the dependency array. Outer scope values like 'confirmLargeVectorDataset' aren't valid dependencies because mutating them doesn't re-render the component

const addDroppedPhotos = useCallback(
(result: GeotaggedPhotoResult | null): number => {
Expand Down Expand Up @@ -1657,7 +1657,7 @@
disposed = true;
unlisten?.();
};
}, [

Check warning on line 1660 in apps/geolibre-desktop/src/components/layout/DesktopShell.tsx

View workflow job for this annotation

GitHub Actions / Build and test

React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array
clearDropMessageLater,
finishDrop,
addDroppedRasters,
Expand Down Expand Up @@ -1801,7 +1801,7 @@
clearDropMessageLater();
}
},
[

Check warning on line 1804 in apps/geolibre-desktop/src/components/layout/DesktopShell.tsx

View workflow job for this annotation

GitHub Actions / Build and test

React Hook useCallback has a missing dependency: 't'. Either include it or remove the dependency array
clearDropMessageLater,
finishDrop,
addDroppedRasters,
Expand All @@ -1811,6 +1811,46 @@
],
);

// Escape hatch for a drop overlay that outlived its drag (issue #1664).
//
// The overlay is driven by one boolean fed from two places: the webview drag
// handlers above (balanced by dragDepthRef) and, on desktop, Tauri's native
// onDragDropEvent (no counter at all, since the OS reports enter/leave/drop
// directly). Either feed can strand it. A native "leave" that never arrives
// — which is what a modal native file dialog opening mid-drag produces on
// WebKitGTK — leaves the flag set with nothing left to clear it, and an
// unbalanced webview enter/leave pair leaves dragDepthRef above zero, which
// has the same effect. The result is an overlay covering the map until the
// user happens to drag another file across the window.
//
// Rather than guess at every way the OS can swallow an event, recover on two
// signals that cannot occur while a real drag is in progress: a key press and
// a pointer button. Neither fires during an HTML5 drag (the spec suppresses
// mouse events for the duration) nor during a native one (the OS holds a
// pointer grab), so this can never dismiss the overlay out from under a drag
// the user is actually performing.
Comment thread
giswqs marked this conversation as resolved.
Outdated
useEffect(() => {
if (!isDraggingFiles) return;

const clear = () => {
dragDepthRef.current = 0;
setIsDraggingFiles(false);
};
const onKeyDown = (event: KeyboardEvent) => {
// Escape is the one the reporter reached for first; any key would do, but
// limiting it keeps typing in a panel from silently cancelling feedback
// for a drag that is genuinely still running.
if (event.key === "Escape") clear();
Comment thread
giswqs marked this conversation as resolved.
Comment thread
giswqs marked this conversation as resolved.
Outdated
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

window.addEventListener("keydown", onKeyDown);
window.addEventListener("pointerdown", clear);
Comment thread
giswqs marked this conversation as resolved.
Outdated
return () => {
window.removeEventListener("keydown", onKeyDown);
window.removeEventListener("pointerdown", clear);
};
Comment thread
giswqs marked this conversation as resolved.
Outdated
}, [isDraggingFiles]);

const startLayerPanelResize = useCallback(
(event: ReactPointerEvent<HTMLDivElement>) => {
event.preventDefault();
Expand Down Expand Up @@ -2569,7 +2609,10 @@
className="pointer-events-none fixed bottom-7 top-11 z-50 hidden w-px bg-primary shadow-[0_0_0_1px_hsl(var(--primary)/0.25)]"
/>
{isDraggingFiles ? (
<div className="pointer-events-none absolute inset-0 z-50 flex items-center justify-center bg-background/70 backdrop-blur-sm">
<div
data-testid="file-drop-overlay"
className="pointer-events-none absolute inset-0 z-50 flex items-center justify-center bg-background/70 backdrop-blur-sm"
>
<div className="max-w-sm rounded-md border bg-background px-4 py-3 text-center shadow-lg">
<p className="text-sm font-medium">{t("toolbar.fileDrop.overlayTitle")}</p>
<p className="mt-1 text-xs text-muted-foreground">
Expand Down
65 changes: 65 additions & 0 deletions e2e/drop-overlay.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { expect, test, type Page } from "@playwright/test";
import { waitForMap } from "./helpers";

/**
* Regression cover for GeoLibre#1664: the drag-and-drop overlay could outlive
* its drag and sit over the map forever.
*
* The overlay is one boolean fed from two places — the webview drag handlers
* (balanced by a depth counter) and, on desktop, Tauri's native drag events (no
* counter). Either feed can strand it: a native "leave" the OS never delivers,
* or an unbalanced webview enter/leave pair that leaves the counter above zero.
* These tests strand it the second way, which is the one a browser can drive,
* and assert the recovery paths that clear it either way.
*/

const OVERLAY = '[data-testid="file-drop-overlay"]';

/** Fires one file-drag event at the shell, without completing a drop. */
async function fireFileDrag(page: Page, type: "dragenter" | "dragleave"): Promise<void> {
const dataTransfer = await page.evaluateHandle(() => {
const dt = new DataTransfer();
dt.items.add(new File(["{}"], "style.json", { type: "application/json" }));
return dt;
});
await page.dispatchEvent('[data-testid="map-canvas"]', type, { dataTransfer });
await dataTransfer.dispose();
}

/**
* Leaves the overlay showing with no drag in progress, by sending one more
* `dragenter` than `dragleave` — the shape a swallowed leave event produces.
*/
async function strandOverlay(page: Page): Promise<void> {
await fireFileDrag(page, "dragenter");
await fireFileDrag(page, "dragenter");
await fireFileDrag(page, "dragleave");
await expect(page.locator(OVERLAY)).toBeVisible();
}

test("Escape dismisses a drop overlay that outlived its drag", async ({ page }) => {
await waitForMap(page);
await strandOverlay(page);

await page.keyboard.press("Escape");
await expect(page.locator(OVERLAY)).toBeHidden();
});

test("a pointer press dismisses a drop overlay that outlived its drag", async ({ page }) => {
await waitForMap(page);
await strandOverlay(page);

// A mouse button cannot go down during a real drag, so this is the signal
// that heals the overlay without the user knowing to press anything.
await page.mouse.click(400, 400);
await expect(page.locator(OVERLAY)).toBeHidden();
});

test("a balanced drag still shows and hides the overlay", async ({ page }) => {
await waitForMap(page);

await fireFileDrag(page, "dragenter");
await expect(page.locator(OVERLAY)).toBeVisible();
await fireFileDrag(page, "dragleave");
await expect(page.locator(OVERLAY)).toBeHidden();
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading