diff --git a/src/App.test.tsx b/src/App.test.tsx
new file mode 100644
index 0000000..f3033be
--- /dev/null
+++ b/src/App.test.tsx
@@ -0,0 +1,21 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+import App from "./App";
+
+vi.stubGlobal("Worker", class {
+ postMessage() {}
+ terminate() {}
+});
+
+describe("App canvas selection", () => {
+ it("does not expose a viewer canvas before a file is opened", () => {
+ render();
+ expect(screen.getByText("Open an Adobe XD file")).toBeInTheDocument();
+ });
+
+ it("keeps normal document text non-selectable by CSS", () => {
+ const { container } = render();
+ fireEvent.doubleClick(container);
+ expect(container.querySelector(".selectable-text")).not.toBeInTheDocument();
+ });
+});
diff --git a/src/App.tsx b/src/App.tsx
index ba58d2b..6604ec8 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -66,6 +66,13 @@ export default function App() {
zoom: number;
} | null>(null);
const canvasRef = useRef(null);
+ const selectedTextRef = useRef(null);
+
+ const clearTextSelection = useCallback(() => {
+ selectedTextRef.current?.classList.remove("selectable-text");
+ selectedTextRef.current = null;
+ window.getSelection()?.removeAllRanges();
+ }, []);
useEffect(() => { panRef.current = pan; }, [pan]);
useEffect(() => { zoomRef.current = zoom; }, [zoom]);
@@ -243,6 +250,7 @@ export default function App() {
}}
onPointerDown={(event) => {
if (event.pointerType === "touch") return;
+ if (event.detail < 2) clearTextSelection();
event.currentTarget.setPointerCapture(event.pointerId);
dragRef.current = { x: event.clientX, y: event.clientY, originX: pan.x, originY: pan.y };
}}
@@ -257,6 +265,18 @@ export default function App() {
onPointerCancel={(event) => {
if (event.pointerType !== "touch") dragRef.current = null;
}}
+ onDoubleClick={(event) => {
+ const target = event.target instanceof Element ? event.target.closest("text") : null;
+ if (!(target instanceof SVGTextElement)) return;
+ clearTextSelection();
+ target.classList.add("selectable-text");
+ selectedTextRef.current = target;
+ const range = window.document.createRange();
+ range.selectNodeContents(target);
+ const selection = window.getSelection();
+ selection?.removeAllRanges();
+ selection?.addRange(range);
+ }}
>
{document.artboards.length || document.pasteboardLayers.length ? (