Fixed desktop text selection behavior.
Some checks failed
Deploy to GitHub Pages / build (push) Failing after 4m50s
Deploy to GitHub Pages / deploy (push) Has been skipped

This commit is contained in:
2026-06-14 18:55:43 +03:30
parent fc4ef29230
commit 1d174697fb
3 changed files with 45 additions and 2 deletions

21
src/App.test.tsx Normal file
View File

@@ -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(<App />);
expect(screen.getByText("Open an Adobe XD file")).toBeInTheDocument();
});
it("keeps normal document text non-selectable by CSS", () => {
const { container } = render(<App />);
fireEvent.doubleClick(container);
expect(container.querySelector(".selectable-text")).not.toBeInTheDocument();
});
});

View File

@@ -66,6 +66,13 @@ export default function App() {
zoom: number;
} | null>(null);
const canvasRef = useRef<HTMLElement>(null);
const selectedTextRef = useRef<SVGTextElement | null>(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 ? (
<div

View File

@@ -61,11 +61,13 @@ h1 { margin: 0; font-size: 34px; letter-spacing: -1.3px; }
.warnings li { padding: 8px 0; border-bottom: 1px solid #292c36; color: #aeb2bf; font-size: 11px; line-height: 1.4; }
.warnings li small { display: block; color: #777c8b; }
.canvas { position: relative; display: grid; place-items: center; overflow: hidden; touch-action: none; overscroll-behavior: none; user-select: none; cursor: grab; background-color: #11131a; background-image: radial-gradient(#343743 1px, transparent 1px); background-size: 20px 20px; }
.canvas { position: relative; display: grid; place-items: center; overflow: hidden; touch-action: none; overscroll-behavior: none; user-select: none; -webkit-user-select: none; cursor: grab; background-color: #11131a; background-image: radial-gradient(#343743 1px, transparent 1px); background-size: 20px 20px; }
.canvas:active { cursor: grabbing; }
.stage { position: relative; transform-origin: center; transition: transform .05s linear; }
.document-stage { flex: 0 0 auto; }
.document-view { display: block; overflow: visible; pointer-events: none; }
.document-view { display: block; overflow: visible; }
.document-view text { user-select: none; -webkit-user-select: none; cursor: default; }
.document-view text.selectable-text { user-select: text; -webkit-user-select: text; cursor: text; }
.document-artboard-label { fill: #969baa; font-size: 12px; }
.artboard { display: block; overflow: visible; background: white; }
.artboard-label { position: absolute; left: 0; top: -28px; max-width: 100%; color: #969baa; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }