5.6 KiB
Component Interaction Model
How the canvas decides whether a mouse click on a component opens its property dialog, starts a drag-to-rearrange, or is forwarded to the wokwi web component's own pointer logic (knob rotation, button press, joystick stick drag, etc.).
TL;DR
When a user clicks on a component on the simulator canvas, exactly one of three things happens:
| Surface clicked | Result |
|---|---|
| Wrapper border / padding / non-interactive area of any component | Canvas owns the click. Single click = open property dialog; mousedown + threshold movement = drag the component to reposition it. |
| A wokwi web component whose tag is in the pointer-owning whitelist below | Wokwi component owns the click. Rotates the knob, presses the button, slides the switch, etc. |
| A wokwi web component NOT in the whitelist (sensor, display, LED, resistor, etc.) | Same as the wrapper case — canvas owns the click. |
The whitelist
The exhaustive list lives in
frontend/src/components/DynamicComponent.tsx
inside handleMouseDown. As of this writing it covers:
const ownsPointer =
interactionRunning &&
(tag === 'wokwi-pushbutton'
|| tag === 'wokwi-pushbutton-6mm'
|| tag === 'wokwi-potentiometer'
|| tag === 'wokwi-slide-potentiometer'
|| tag === 'wokwi-slide-switch'
|| tag === 'wokwi-dip-switch-8'
|| tag === 'wokwi-analog-joystick'
|| tag === 'wokwi-ky-040'
|| tag === 'wokwi-membrane-keypad'
|| tag === 'wokwi-rotary-dialer');
If you wire a NEW wokwi component that depends on its own internal
pointerdown / pointermove / pointerup handlers, append its custom
element tag here. Otherwise the canvas's e.stopPropagation() in the
capture phase will swallow the pointer event before the wokwi component
ever sees it — the knob will not rotate, the button won't report a
press, the joystick stick won't move.
Why the gate is opt-in (whitelist) rather than opt-out
The gate used to be heuristic: "if the component has a
PartSimulationRegistry handler (isInteractive), let the wokwi
component own the pointerdown." That heuristic was too broad — DHT22,
HC-SR04, NTC, photoresistor, LED all register attachEvents to bridge
the SPICE / sensor-update layers, but visually they have NO internal
pointer logic. The heuristic caused clicks on those components to be
silently swallowed by the wokwi shadow DOM (which did nothing with
them) → property dialog never opened → users couldn't change sensor
values.
A tight whitelist is verifiable in one read and trivial to extend.
Property dialog flow
For non-pointer-owning components, the mousedown bubbles to
SimulatorCanvas which:
- Tracks the down position and starts a movement-threshold timer.
- If the user releases the button without moving past ~3 px ⇒ treat as
click ⇒ open
<ComponentPropertyDialog>for that component. - If the user moves past the threshold ⇒ treat as drag ⇒ reposition the component on the canvas grid.
The property dialog renders a SensorControlPanel (for sensors that
have SENSOR_CONTROL_DEFS) or a generic property editor. The slider
dispatches dispatchSensorUpdate(componentId, { … }) which routes via
SensorUpdateRegistry to the part's attachEvents callback. That
callback updates the AVR ADC (setAdcVoltage) AND emits a
PROPERTY_CHANGE_EVENT so the SPICE netlist gets rebuilt with the new
value (e.g. NTC resistance recalculated from the new temperature).
Live-simulation vs. edit-mode
The whitelist is only consulted when interactionRunning is true —
either the MCU sim is running or this is a board-less SPICE-only
circuit with the electrical simulation un-paused. While the user is in
"edit mode" (sim stopped) the canvas always owns clicks so the user can
arrange components without accidentally rotating knobs.
Test infrastructure note
CDP-synthesized PointerEvent carries isTrusted = false. Lit's
gesture machinery (used by wokwi-elements) requires real OS-issued
trusted events. Automated tests that need to rotate a potentiometer or
press a button cannot do it via dispatchEvent. Two workarounds:
- For pushbuttons: dispatch the custom
button-press/button-releaseevents directly on the wokwi-pushbutton DOM element. The Velxio simulation layer reacts to those. - For rotary / slider components: reach into the wokwi shadow root,
find the hidden
<input type="range">, set itsvaluevia the native HTMLInputElement setter, then dispatch a bubblinginputevent. Example:
const slider = document.querySelector('wokwi-potentiometer')
.shadowRoot.querySelector('input[type="range"]');
const set = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
set.call(slider, '512');
slider.dispatchEvent(new Event('input', { bubbles: true }));
Real user mouse drag works correctly without any of this — the whitelist fix is all that was needed for the runtime path.
Related files
frontend/src/components/DynamicComponent.tsx— the per-component wrapper,handleMouseDownlives here.frontend/src/components/simulator/SimulatorCanvas.tsx— owns the canvas drag-threshold logic and dialog open dispatch.frontend/src/components/simulator/SensorControlPanel.tsx— renders the slider/button UI for sensor property editing.frontend/src/simulation/SensorUpdateRegistry.ts— module-level singleton bridging the dialog → partattachEventscallbacks.frontend/src/simulation/parts/SensorParts.ts,BasicParts.ts,ComplexParts.ts— the actual per-part handlers that consume slider updates.