fix: pan board from empty space

This commit is contained in:
2026-08-14 17:59:03 +02:00
parent a15b463589
commit 38f50848d0
3 changed files with 13 additions and 7 deletions
+5 -6
View File
@@ -24,7 +24,7 @@ async function waitForSave(page: Page, action: () => Promise<void>) {
await response
}
test('move, folder expansion, hand pan, desktop wheel zoom, mobile pinch, and reload persistence', async ({ page }) => {
test('move, folder expansion, empty-board pan, desktop wheel zoom, mobile pinch, and reload persistence', async ({ page }) => {
await page.goto('/?level=e2e-level&edit=1')
await expect(page.getByRole('heading', { name: 'Browser Safety Test' })).toBeVisible()
await expect(page.locator('.documents-panel')).toHaveClass(/\bclosed\b/)
@@ -65,14 +65,13 @@ test('move, folder expansion, hand pan, desktop wheel zoom, mobile pinch, and re
const folderBeforePan = await boardPosition(folder)
const transformBeforePan = await board.getAttribute('style')
await page.getByRole('button', { name: 'HAND', exact: true }).click()
await expect(page.getByRole('button', { name: 'MOVE', exact: true })).toHaveClass(/\bactive\b/)
const viewportBox = await boardViewport.boundingBox()
if (!viewportBox) throw new Error('Board viewport is not visible')
await waitForSave(page, async () => {
await page.mouse.move(viewportBox.x + 240, viewportBox.y + 230)
await page.mouse.down()
await page.mouse.move(viewportBox.x + 290, viewportBox.y + 195, { steps: 5 })
await page.mouse.up()
await boardViewport.dispatchEvent('pointerdown', { pointerId: 31, pointerType: 'mouse', isPrimary: true, button: 0, clientX: viewportBox.x + 240, clientY: viewportBox.y + 230 })
await boardViewport.dispatchEvent('pointermove', { pointerId: 31, pointerType: 'mouse', isPrimary: true, button: 0, clientX: viewportBox.x + 290, clientY: viewportBox.y + 195 })
await boardViewport.dispatchEvent('pointerup', { pointerId: 31, pointerType: 'mouse', isPrimary: true, button: 0, clientX: viewportBox.x + 290, clientY: viewportBox.y + 195 })
})
expect(await boardPosition(folder)).toEqual(folderBeforePan)
const transformAfterPan = await board.getAttribute('style')
+6 -1
View File
@@ -518,7 +518,12 @@ function Board({ state, selected, linkFrom, recentlyCreatedExhibitId, recentlyCr
const toggleFolder = (id: string) => update(s => ({ ...s, evidence: s.evidence.map(widget => widget.id === id ? { ...widget, config: { ...(widget.config || {}), open: !folderIsOpen(widget) } } : widget) }))
const previewOrigin = linkFrom ? pointForId(linkFrom) : undefined
return <div className={`board-viewport tool-${tool} ${linkFrom ? 'threading' : ''}`} ref={boardRef}
onPointerDown={e => { if (!(e.target as HTMLElement).closest('.thread-tag')) setExpandedThreadTagId(null); if (e.pointerType === 'touch' || tool === 'hand' || e.button === 1) { e.preventDefault(); pointerDown(e) } }}
onPointerDown={e => {
const target = e.target as HTMLElement
if (!target.closest('.thread-tag')) setExpandedThreadTagId(null)
const emptyBoardDrag = e.button === 0 && !target.closest('.evidence-card, .source-file-widget, .thread-tag, button')
if (e.pointerType === 'touch' || tool === 'hand' || e.button === 1 || emptyBoardDrag) { e.preventDefault(); pointerDown(e) }
}}
onPointerMoveCapture={trackThreadPointer} onPointerMove={pointerMove} onPointerLeave={() => setThreadPointer(null)} onPointerUp={finishDrag} onPointerCancel={finishDrag} onAuxClick={e => { if (e.button === 1) e.preventDefault() }}>
<div className="board" style={{ width: BOARD_W, height: BOARD_H, transform: `translate(${state.viewport.x}px, ${state.viewport.y}px) scale(${state.viewport.zoom})` }}>
<div className="board-stamp">AUTHORIZED CITIZEN SCIENTIST WORKSTATION <span>GU-NET / 04</span></div>
+2
View File
@@ -50,6 +50,8 @@ button:focus-visible { outline: 2px solid #e99a44; outline-offset: 2px; }
.case-number { border-left: 1px solid #415750; margin-left: 26px; padding-left: 17px; font: 8px IBM Plex Mono; color: #6d867f; line-height: 1.5; }
.case-number b { color: #bdc9c3; font-size: 13px; }
.board-viewport { position: absolute; inset: 0; overflow: hidden; touch-action: none; overscroll-behavior: contain; cursor: default; background-image: radial-gradient(#49615a55 1px, transparent 1px), linear-gradient(90deg, #18302a33 1px, transparent 1px), linear-gradient(#18302a33 1px, transparent 1px); background-size: 20px 20px, 100px 100px, 100px 100px; }
.board-viewport.tool-move { cursor: grab; }
.board-viewport.tool-move:active { cursor: grabbing; }
.board-viewport.tool-hand, .board-viewport.tool-hand .evidence-card, .board-viewport.tool-hand .source-file-widget { cursor: grab; }
.board-viewport.tool-hand:active, .board-viewport.tool-hand .evidence-card:active, .board-viewport.tool-hand .source-file-widget:active { cursor: grabbing; }
.board-viewport.threading, .board-viewport.threading .evidence-card, .board-viewport.threading .source-file-widget { cursor: crosshair; }