fix(editor): guard drill selection text editing

This commit is contained in:
Danila Poyarkov 2026-05-13 05:07:27 +03:00
parent aa9f03407a
commit 1f273f880e
3 changed files with 107 additions and 4 deletions

View file

@ -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
}

View file

@ -41,6 +41,7 @@ export function useCanvasInput(
) {
const drag = ref<DragState | null>(null)
const cursorOverride = ref<string | null>(null)
const selectedIdsBeforeClickSequence = ref<ReadonlySet<string>>(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,

View file

@ -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)
})