fix(app): restore left panel scrolling

This commit is contained in:
Danila Poyarkov 2026-05-13 05:47:32 +03:00
parent 8cf51ffefd
commit 9ab6bfa3bf
6 changed files with 144 additions and 24 deletions

View file

@ -6,6 +6,7 @@ import { createNanoEvents } from 'nanoevents'
import * as HitTest from './hit-test'
import * as Instances from './instances'
import { CONTAINER_TYPES, createDefaultNode } from './node-defaults'
import { updateNodePreview } from './preview'
import * as Variables from './variables'
import { normalizeVectorNetwork } from './vector-network'
@ -363,25 +364,7 @@ export class SceneGraph {
}
updateNodePreview(id: string, changes: Partial<SceneNode>): void {
const node = this.nodes.get(id)
if (!node) return
if ((Object.keys(changes) as (keyof SceneNode)[]).every((key) => node[key] === changes[key])) {
return
}
const affectsLayout = Object.keys(changes).some((k) => SceneGraph.LAYOUT_AFFECTING_KEYS.has(k))
if (affectsLayout) this.absPosCache.clear()
if (
node.type === 'TEXT' &&
node.textPicture &&
Object.keys(changes).some((k) => SceneGraph.TEXT_PICTURE_KEYS.has(k))
) {
node.textPicture = null
}
if (changes.vectorNetwork) {
changes = { ...changes, vectorNetwork: normalizeVectorNetwork(changes.vectorNetwork) }
}
this.positionPreviewVersion++
Object.assign(node, changes)
updateNodePreview(this, id, changes)
}
updateNode(id: string, changes: Partial<SceneNode>): void {

View file

@ -0,0 +1,89 @@
import { normalizeVectorNetwork } from './vector-network'
import type { SceneNode } from './types'
type PreviewGraph = {
nodes: Map<string, SceneNode>
positionPreviewVersion: number
clearAbsPosCache: () => void
}
const LAYOUT_AFFECTING_KEYS = new Set<string>([
'x',
'y',
'width',
'height',
'rotation',
'parentId',
'childIds',
'layoutMode',
'layoutDirection',
'layoutWrap',
'primaryAxisSizing',
'counterAxisSizing',
'itemSpacing',
'counterAxisSpacing',
'paddingTop',
'paddingRight',
'paddingBottom',
'paddingLeft',
'layoutGrow',
'layoutAlignSelf',
'layoutPositioning',
'minWidth',
'maxWidth',
'minHeight',
'maxHeight',
'visible',
'text',
'fontSize',
'lineHeight',
'letterSpacing',
'styleRuns',
'textAutoResize'
])
const TEXT_PICTURE_KEYS = new Set<string>([
'text',
'fontSize',
'fontFamily',
'fontWeight',
'italic',
'textAlignHorizontal',
'textDirection',
'textAlignVertical',
'lineHeight',
'letterSpacing',
'textDecoration',
'textCase',
'styleRuns',
'fills',
'width',
'height'
])
export function updateNodePreview(
graph: PreviewGraph,
id: string,
changes: Partial<SceneNode>
): void {
const node = graph.nodes.get(id)
if (!node) return
if ((Object.keys(changes) as (keyof SceneNode)[]).every((key) => node[key] === changes[key])) {
return
}
const affectsLayout = Object.keys(changes).some((key) => LAYOUT_AFFECTING_KEYS.has(key))
if (affectsLayout) graph.clearAbsPosCache()
if (
node.type === 'TEXT' &&
node.textPicture &&
Object.keys(changes).some((key) => TEXT_PICTURE_KEYS.has(key))
) {
node.textPicture = null
}
const normalizedChanges = changes.vectorNetwork
? { ...changes, vectorNetwork: normalizeVectorNetwork(changes.vectorNetwork) }
: changes
graph.positionPreviewVersion++
Object.assign(node, normalizedChanges)
}

View file

@ -163,6 +163,8 @@ provideLayerTree({
<TreeRoot
:key="treeKey"
v-slot="{ flattenItems }"
as="div"
class="flex min-h-0 flex-1 flex-col overflow-hidden"
:expanded="expanded"
:items="items"
:get-key="getKey"

View file

@ -50,8 +50,9 @@ function onTreeSelect(e: CustomEvent, select: (additive: boolean) => void) {
:indent-per-level="INDENT"
>
<ContextMenuRoot :modal="false">
<ContextMenuTrigger as-child @contextmenu="onLayerRightClick">
<div v-bind="attrs" class="relative scrollbar-thin flex-1 overflow-y-auto px-1">
<div v-bind="attrs" class="relative min-h-0 flex-1 overflow-hidden">
<ContextMenuTrigger as-child @contextmenu="onLayerRightClick">
<div data-test-id="layers-scroll" class="scrollbar-thin h-full overflow-y-auto px-1">
<template v-if="flattenItems">
<LayerTreeItem
v-for="item in flattenItems"
@ -211,8 +212,9 @@ function onTreeSelect(e: CustomEvent, select: (additive: boolean) => void) {
</TreeItem>
</LayerTreeItem>
</template>
</div>
</ContextMenuTrigger>
</div>
</ContextMenuTrigger>
</div>
<ContextMenuPortal>
<CanvasMenu />
</ContextMenuPortal>

View file

@ -52,7 +52,8 @@ function handlePageDblClick(
</button>
</Tip>
</div>
<div class="scrollbar-thin overflow-x-hidden overflow-y-auto px-1 pb-1">
<div class="min-h-0 flex-1 overflow-hidden">
<div data-test-id="pages-scroll" class="scrollbar-thin h-full overflow-x-hidden overflow-y-auto px-1 pb-1">
<div v-for="pg in pages" :key="pg.id">
<div
v-if="rename.editingId.value === pg.id"
@ -91,6 +92,7 @@ function handlePageDblClick(
<span class="truncate">{{ pg.name }}</span>
</button>
</div>
</div>
</div>
</div>
</PageListRoot>

View file

@ -0,0 +1,42 @@
import { expect, test } from '@playwright/test'
import { CanvasHelper } from '#tests/helpers/canvas'
test('pages and layers panels scroll inside splitter panes', async ({ page }) => {
await page.goto('/')
const canvas = new CanvasHelper(page)
await canvas.waitForInit()
canvas.errors.length = 0
await page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
for (let i = 0; i < 40; i++) store.addPage(`Page ${i + 2}`)
const pageId = store.state.currentPageId
for (let i = 0; i < 80; i++) {
store.graph.createNode('RECTANGLE', pageId, {
name: `Layer ${i + 1}`,
x: i * 2,
y: i * 2,
width: 20,
height: 20
})
}
store.requestRender()
})
await canvas.waitForRender()
const pagesScroller = page.locator('[data-test-id="pages-scroll"]')
const layersScroller = page.locator('[data-test-id="layers-scroll"]')
await pagesScroller.evaluate((el) => {
el.scrollTop = el.scrollHeight
})
await layersScroller.evaluate((el) => {
el.scrollTop = el.scrollHeight
})
await expect.poll(() => pagesScroller.evaluate((el) => el.scrollTop)).toBeGreaterThan(0)
await expect.poll(() => layersScroller.evaluate((el) => el.scrollTop)).toBeGreaterThan(0)
canvas.assertNoErrors()
})