fix(canvas): three desktop interaction bugs

Mobile was working fine; desktop had a string of issues that surfaced
together on the Pico Doom example after the simulator/wiring fixes.

1. Selection action bar appeared during simulation, intercepting button
   presses. handleComponentMouseDown unconditionally called
   e.stopPropagation() + setSelectedComponentId, so clicking a wokwi-
   pushbutton on a running canvas ate the mousedown — the
   button-press event never fired and the floating Rotate/Delete bar
   popped up on top of the button. Now: while running, the handler
   returns early so the event propagates to the underlying component
   and the canvas stays read-only.

2. The selection action bar was always visible on desktop. It was
   introduced as the primary delete UI for touch devices (no Delete
   key, no right-click), but it kept showing on mouse-and-keyboard
   too — covering pins and intercepting clicks. Now gated on
   isTouchDevice (already wired via useIsCoarsePointer) AND !running.
   Desktop users keep Delete key + right-click context menu for the
   same operations.

3. Left-click drag on the canvas background didn't pan. Pan was
   limited to middle/right click. Now left-click on empty canvas
   panning works too (component mousedowns stopPropagation so they
   still drag the component, not the camera). Wiring mode keeps left
   click for waypoint drops, so the pan only kicks in when not in
   wire mode and not in a property dialog. Matches Figma / Miro /
   draw.io convention.

Build verified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Montero Crespo 2026-05-15 23:59:22 -03:00
parent fa224acb8d
commit 77a63ca10b
1 changed files with 28 additions and 4 deletions

View File

@ -1258,6 +1258,14 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
const handleComponentMouseDown = (componentId: string, e: React.MouseEvent) => {
if (showPropertyDialog) return;
// While the simulator is running, the canvas is read-only. Don't
// intercept the mousedown — let it propagate to the underlying
// component (wokwi-pushbutton etc.) so it can fire button-press /
// change events. Without this, every press on a button on the Pico
// Doom canvas during a run was getting eaten by the drag/selection
// handler.
if (running) return;
e.stopPropagation();
const component = components.find((c) => c.id === componentId);
if (!component) return;
@ -1543,7 +1551,19 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
// Start panning on middle-click or right-click
const handleCanvasMouseDown = (e: React.MouseEvent) => {
if (e.button === 1 || e.button === 2) {
// Middle / right click — always pan, regardless of context.
// Left click — pan only when clicking ON THE BACKGROUND (the event
// reaches the canvas because component mousedowns stopPropagation),
// and only when not wiring (wire mode uses left click to drop
// waypoints) or running a property dialog. Matches the diagram-editor
// convention used in Figma / Miro / draw.io.
const leftButton = e.button === 0;
const middleOrRight = e.button === 1 || e.button === 2;
const isPanGesture =
middleOrRight ||
(leftButton && !wireInProgress && !showPropertyDialog);
if (isPanGesture) {
e.preventDefault();
isPanningRef.current = true;
panStartRef.current = {
@ -2454,9 +2474,13 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
)}
{/* Floating action bar for the current selection primary delete UI
for touch devices (no Delete key, no right-click). Hidden while
creating a wire so it doesn't fight the wire-mode banner. */}
{!wireInProgress &&
for touch devices (no Delete key, no right-click). Hidden:
- while creating a wire (would fight the wire-mode banner)
- on desktop (delete is bound to the Delete key, rotate is in
the right-click menu the floating bar covered nearby pins
and intercepted clicks on buttons during simulation)
- while the simulator is running (canvas is read-only). */}
{!wireInProgress && isTouchDevice && !running &&
(() => {
if (selectedWireId) {
const wire = wires.find((w) => w.id === selectedWireId);