Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions suite/apps/tableaux/components/canvas/RoomCanvas.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { render, fireEvent, act } from '@testing-library/react'
import RoomCanvas from './RoomCanvas'
import { useStore } from '../../store/useStore'
import { openPlan } from '../../test/openPlan'

const pointer = (type: string, x: number, y: number) =>
act(() => {
window.dispatchEvent(new MouseEvent(type, { clientX: x, clientY: y }))
})

// A cancelled pointer (a system gesture, an interrupted touch) is not a
// release: the draft must go, and nothing it would have committed may happen.
describe('RoomCanvas drawing, when the pointer is cancelled', () => {
beforeEach(() => {
openPlan({})
})

it('drops a half-drawn zone and adds nothing', () => {
useStore.setState({ activeTool: 'zone' })
const { container } = render(<RoomCanvas />)
const canvas = container.querySelector('[data-tour="seating.canvas"]')!

fireEvent.pointerDown(canvas, { button: 0, clientX: 10, clientY: 10 })
pointer('pointermove', 200, 200)
expect(container.querySelector('[class*="draftZone"]')).not.toBeNull()

pointer('pointercancel', 200, 200)
expect(container.querySelector('[class*="draftZone"]')).toBeNull()

// A release after the cancel belongs to no drag.
pointer('pointerup', 200, 200)
expect(Object.keys(useStore.getState().zones)).toHaveLength(0)
expect(useStore.getState().activeTool).toBe('select')
})

it('drops a half-drawn calibration line and opens no dialog', () => {
useStore.setState({ activeTool: 'calibrate' })
const { container } = render(<RoomCanvas />)
const canvas = container.querySelector('[data-tour="seating.canvas"]')!

fireEvent.pointerDown(canvas, { button: 0, clientX: 10, clientY: 10 })
pointer('pointermove', 200, 200)
expect(container.querySelector('line[stroke-dasharray]')).not.toBeNull()

pointer('pointercancel', 200, 200)
expect(container.querySelector('line[stroke-dasharray]')).toBeNull()

pointer('pointerup', 200, 200)
expect(useStore.getState().modal).toBeNull()
expect(useStore.getState().activeTool).toBe('select')
})

it('still adds a zone on an ordinary release', () => {
useStore.setState({ activeTool: 'zone' })
const { container } = render(<RoomCanvas />)
const canvas = container.querySelector('[data-tour="seating.canvas"]')!

fireEvent.pointerDown(canvas, { button: 0, clientX: 10, clientY: 10 })
pointer('pointermove', 200, 200)
pointer('pointerup', 200, 200)
expect(Object.keys(useStore.getState().zones)).toHaveLength(1)
})
})
28 changes: 18 additions & 10 deletions suite/apps/tableaux/components/canvas/RoomCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,21 +141,25 @@ export default function RoomCanvas() {
[canvas.panX, canvas.panY, canvas.zoom]
)

// TODO(ux-audit): startZoneDraw and startCalibrate below both register only
// pointermove/pointerup on window, no pointercancel (same bug class fixed
// in TableHandles.jsx, see tmp/ux-audit.md #C13) — a pointer release/
// cancel outside the window can strand a draft zone or calibration line
// mid-draw. See tmp/ux-audit.md #C14.
// A cancelled pointer (a system gesture, an interrupted touch) is not a
// release: it drops the draft and commits nothing. Without listening for it
// the draft stayed on the canvas, still following the pointer.
const startZoneDraw = (e: ReactPointerEvent) => {
const start = screenToCanvas(e.clientX, e.clientY)
const onMove = (ev: PointerEvent) => {
const rect = normRect(start, screenToCanvas(ev.clientX, ev.clientY))
draftRef.current = rect
setDraftZone(rect)
}
const onUp = () => {
const stop = () => {
window.removeEventListener('pointermove', onMove)
window.removeEventListener('pointerup', onUp)
window.removeEventListener('pointercancel', stop)
draftRef.current = null
setDraftZone(null)
setActiveTool('select')
}
const onUp = () => {
const r = draftRef.current
if (r && r.width > 16 && r.height > 16) {
addZone({
Expand All @@ -167,12 +171,11 @@ export default function RoomCanvas() {
shape: 'rect',
})
}
draftRef.current = null
setDraftZone(null)
setActiveTool('select')
stop()
}
window.addEventListener('pointermove', onMove)
window.addEventListener('pointerup', onUp)
window.addEventListener('pointercancel', stop)
}

const startCalibrate = (e: ReactPointerEvent) => {
Expand All @@ -183,18 +186,23 @@ export default function RoomCanvas() {
line = { x1: start.x, y1: start.y, x2: end.x, y2: end.y }
setDraftLine(line)
}
const onUp = () => {
const stop = () => {
window.removeEventListener('pointermove', onMove)
window.removeEventListener('pointerup', onUp)
window.removeEventListener('pointercancel', stop)
setDraftLine(null)
setActiveTool('select')
}
const onUp = () => {
stop()
if (line) {
const dist = Math.hypot(line.x2 - line.x1, line.y2 - line.y1)
if (dist > 8) openModal('calibrate', { pixelDistance: dist })
}
}
window.addEventListener('pointermove', onMove)
window.addEventListener('pointerup', onUp)
window.addEventListener('pointercancel', stop)
}

// ── polygon room drawing: click to drop vertices, Enter/click-start to close ──
Expand Down
5 changes: 0 additions & 5 deletions suite/apps/tableaux/hooks/useCanvasPanZoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,6 @@ export function useCanvasPanZoom() {
}
window.addEventListener('pointermove', onMove)
window.addEventListener('pointerup', onUp)
// TODO(ux-audit): no pointercancel listener (same bug class fixed in
// TableHandles.jsx — see tmp/ux-audit.md #C13). A pointer release/
// cancel outside the window here leaves isPanning stuck true (cursor
// stuck "grabbing"). RoomCanvas.jsx's zone-draw/calibration-line
// dragging has the same gap. See tmp/ux-audit.md #C14.
window.addEventListener('pointercancel', onUp)
},
[setCanvas]
Expand Down
Loading