diff --git a/packages/vue/src/canvas/text-edit/input.ts b/packages/vue/src/canvas/text-edit/input.ts index f9ecd7577..c3ca37807 100644 --- a/packages/vue/src/canvas/text-edit/input.ts +++ b/packages/vue/src/canvas/text-edit/input.ts @@ -15,6 +15,7 @@ type TextEditInputOptions = { hitTestSectionTitle: (cx: number, cy: number) => SceneNode | null hitTestComponentLabel: (cx: number, cy: number) => SceneNode | null getClickCount: () => number + wasSelectedBeforeClickSequence: (id: string) => boolean setDrag: SetDrag } @@ -26,6 +27,7 @@ export function createTextEditInput(options: TextEditInputOptions) { hitTestSectionTitle, hitTestComponentLabel, getClickCount, + wasSelectedBeforeClickSequence, setDrag } = options @@ -97,9 +99,7 @@ export function createTextEditInput(options: TextEditInputOptions) { ? getContainerDescendantHit(selectedId, cx, cy) : hitTestInScope(cx, cy, false) editor.enterContainer(selectedId) - if (hit?.type === 'TEXT') { - startTextEditingAt(hit, cx, cy) - } else if (hit) { + if (hit) { editor.select([hit.id]) } else { editor.clearSelection() @@ -112,6 +112,11 @@ export function createTextEditInput(options: TextEditInputOptions) { if (!hit) return if (hit.type === 'TEXT') { + const isTopLevelText = hit.parentId === editor.state.currentPageId + if (!isTopLevelText && !wasSelectedBeforeClickSequence(hit.id)) { + editor.select([hit.id]) + return + } startTextEditingAt(hit, cx, cy) return } diff --git a/packages/vue/src/canvas/useCanvasInput.ts b/packages/vue/src/canvas/useCanvasInput.ts index db11e0134..4f38774fc 100644 --- a/packages/vue/src/canvas/useCanvasInput.ts +++ b/packages/vue/src/canvas/useCanvasInput.ts @@ -41,6 +41,7 @@ export function useCanvasInput( ) { const drag = ref(null) const cursorOverride = ref(null) + const selectedIdsBeforeClickSequence = ref>(new Set()) const spaceHeld = useSpaceHeld() const { recordClick, getClickCount } = createClickCounter() @@ -63,6 +64,7 @@ export function useCanvasInput( hitTestSectionTitle, hitTestComponentLabel, getClickCount, + wasSelectedBeforeClickSequence: (id) => selectedIdsBeforeClickSequence.value.has(id), setDrag }) @@ -79,7 +81,9 @@ export function useCanvasInput( editor.setHoveredNode(null) const { sx, sy, cx, cy } = getCoords(e) - recordClick(sx, sy) + const selectedIdsBeforeMouseDown = new Set(editor.state.selectedIds) + const clickCount = recordClick(sx, sy) + if (clickCount === 1) selectedIdsBeforeClickSequence.value = selectedIdsBeforeMouseDown handleToolMouseDown({ event: e, cx, diff --git a/tests/e2e/text/double-click-edit.spec.ts b/tests/e2e/text/double-click-edit.spec.ts new file mode 100644 index 000000000..6724cdb2b --- /dev/null +++ b/tests/e2e/text/double-click-edit.spec.ts @@ -0,0 +1,94 @@ +import { expect, test, type Page } from '@playwright/test' + +import { CanvasHelper } from '#tests/helpers/canvas' +import { getEditingTextId } from '#tests/helpers/store' + +async function addTopLevelText(page: Page) { + return page.evaluate(() => { + const store = window.openPencil?.getStore?.() + if (!store) throw new Error('OpenPencil store not initialized') + store.state.zoom = 1 + store.state.panX = 0 + store.state.panY = 0 + const id = store.createShape('TEXT', 200, 200, 150, 30) + store.graph.updateNode(id, { text: 'Hello World', fontSize: 18 }) + store.select([id]) + store.requestRender() + return id + }) +} + +async function addFrameWithNestedText(page: Page) { + return page.evaluate(() => { + const store = window.openPencil?.getStore?.() + if (!store) throw new Error('OpenPencil store not initialized') + store.state.zoom = 1 + store.state.panX = 0 + store.state.panY = 0 + + const pageId = store.state.currentPageId + const frame = store.graph.createNode('FRAME', pageId, { + name: 'Card', + x: 100, + y: 100, + width: 220, + height: 120, + fills: [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 1 }, opacity: 1, visible: true }] + }) + const text = store.graph.createNode('TEXT', frame.id, { + name: 'Nested label', + text: 'Nested label', + x: 20, + y: 20, + width: 140, + height: 30, + fontSize: 18 + }) + store.select([frame.id]) + store.requestRender() + return { frameId: frame.id, textId: text.id } + }) +} + +test('double-clicking top-level text enters text edit mode', async ({ page }) => { + await page.goto('/') + const canvas = new CanvasHelper(page) + await canvas.waitForInit() + canvas.errors.length = 0 + await canvas.clearCanvas() + + const textId = await addTopLevelText(page) + await canvas.waitForRender() + await canvas.pressKey('Escape') + await canvas.dblclick(275, 215) + + await expect.poll(() => getEditingTextId(page), { timeout: 3000 }).toBe(textId) +}) + +test('double-click drill selects nested text before editing it', async ({ page }) => { + await page.goto('/') + const canvas = new CanvasHelper(page) + await canvas.waitForInit() + canvas.errors.length = 0 + await canvas.clearCanvas() + + const ids = await addFrameWithNestedText(page) + await canvas.waitForRender() + + await canvas.dblclick(125, 125) + + await expect.poll(() => getEditingTextId(page), { timeout: 1000 }).toBeNull() + await expect + .poll(() => + page.evaluate(() => { + const store = window.openPencil?.getStore?.() + if (!store) throw new Error('OpenPencil store not initialized') + return [...store.state.selectedIds] + }) + ) + .toEqual([ids.textId]) + + await page.waitForTimeout(600) + await canvas.dblclick(125, 125) + await expect.poll(() => getEditingTextId(page), { timeout: 3000 }).toBe(ids.textId) +})