Dock button clicks dead: real mouse / playwright clicks don't trigger handler, only JS .click() works #79

Closed
opened 2026-04-21 01:22:30 +00:00 by zaelgohary · 2 comments
Member

Repro on current development, WASM built fresh (unset RUSTC_WRAPPER + dx clean + dx build --package hero_os_app --web --features web --no-default-features).

Symptom: clicking any dock archipelago button (Projects, Communication, ...) in a real browser does nothing — popup never opens. Confirmed via Playwright with four click strategies:

strategy panels_after
locator.click() on button 0
locator.click({force:true}) on .dock-icon-box 0
dblclick 0
page.evaluate(btn => btn.click()) 1 (after 1.5s)

Only direct HTMLElement.click() JS invocation triggers the handler. Real mouse events (simulated via CDP or native) do not. data-dioxus-id is present on the <button>, so event delegation should work.

Ruled out:

  • LiveKit SDK scripts (removed both <script> tags from served index.html — bug persists)
  • Stale WASM cache (dx clean + full rebuild — bug persists)
  • Cargo patches (verified hero_archipelagos_core/projects/tasks/stories resolve to local checkout)
  • pointer-events CSS (.dock-btn, .dock-icon-box all have default pointer-events: auto)

Impact: full dock unusable. Users can't open any archipelago. Workaround in tests: dispatch hero:open-island CustomEvent directly.

Repro on current development, WASM built fresh (`unset RUSTC_WRAPPER` + `dx clean` + `dx build --package hero_os_app --web --features web --no-default-features`). Symptom: clicking any dock archipelago button (Projects, Communication, ...) in a real browser does nothing — popup never opens. Confirmed via Playwright with four click strategies: | strategy | panels_after | | --- | --- | | `locator.click()` on button | 0 | | `locator.click({force:true})` on `.dock-icon-box` | 0 | | `dblclick` | 0 | | `page.evaluate(btn => btn.click())` | 1 (after 1.5s) | Only direct `HTMLElement.click()` JS invocation triggers the handler. Real mouse events (simulated via CDP or native) do not. `data-dioxus-id` is present on the `<button>`, so event delegation should work. Ruled out: - LiveKit SDK scripts (removed both `<script>` tags from served `index.html` — bug persists) - Stale WASM cache (`dx clean` + full rebuild — bug persists) - Cargo patches (verified hero_archipelagos_core/projects/tasks/stories resolve to local checkout) - `pointer-events` CSS (.dock-btn, .dock-icon-box all have default pointer-events: auto) Impact: full dock unusable. Users can't open any archipelago. Workaround in tests: dispatch `hero:open-island` CustomEvent directly.
Author
Member

Found the root cause. Same repro as the original report.

Where: crates/hero_os_app/src/main.rs:1314-1319, installed by PR #76 (dock overflow scroll + drag affordance).

The dock-scroll JS calls el.setPointerCapture(e.pointerId) on every pointerdown:

el.addEventListener('pointerdown', function (e) {
    if (e.pointerType === 'touch') return;
    el.__heroDrag = { startX: e.clientX, startScroll: el.scrollLeft, id: e.pointerId };
    el.dataset.dragging = 'true';
    try { el.setPointerCapture(e.pointerId); } catch (err) {}
});

Per the W3C Pointer Events spec, once a pointer is captured on .dock-sections-scroll, the resulting click event's target is the capture element — not the .dock-btn child. Dioxus's event delegation keys on data-dioxus-id walked from event.target, so the click bubbles out of the scroll container with no ancestor carrying the button handler, and the popup never opens.

This is why HTMLElement.click() worked in the earlier probe: it synthesizes a click directly on the button, bypassing pointer capture. Real mouse / Playwright clicks go through the capture path and land on the wrong target.

Minimal fix: arm setPointerCapture only when drag is actually confirmed. Move it into pointermove guarded by a small movement threshold (e.g. Math.hypot(dx, dy) > 3), and drop it from pointerdown. That preserves drag-to-scroll while leaving plain clicks alone.

el.addEventListener('pointerdown', function (e) {
    if (e.pointerType === 'touch') return;
    el.__heroDrag = { startX: e.clientX, startScroll: el.scrollLeft, id: e.pointerId, captured: false };
});

el.addEventListener('pointermove', function (e) {
    const d = el.__heroDrag;
    if (!d || e.pointerId !== d.id) return;
    const dx = e.clientX - d.startX;
    if (!d.captured && Math.abs(dx) > 3) {
        try { el.setPointerCapture(e.pointerId); } catch (err) {}
        el.dataset.dragging = 'true';
        d.captured = true;
    }
    if (d.captured) {
        el.scrollLeft = d.startScroll - dx;
        e.preventDefault();
    }
});
Found the root cause. Same repro as the original report. **Where:** [crates/hero_os_app/src/main.rs:1314-1319](/lhumina_code/hero_os/src/branch/development/crates/hero_os_app/src/main.rs#L1314-L1319), installed by PR #76 (dock overflow scroll + drag affordance). The dock-scroll JS calls `el.setPointerCapture(e.pointerId)` on every `pointerdown`: ```js el.addEventListener('pointerdown', function (e) { if (e.pointerType === 'touch') return; el.__heroDrag = { startX: e.clientX, startScroll: el.scrollLeft, id: e.pointerId }; el.dataset.dragging = 'true'; try { el.setPointerCapture(e.pointerId); } catch (err) {} }); ``` Per the W3C Pointer Events spec, once a pointer is captured on `.dock-sections-scroll`, the resulting `click` event's `target` is the capture element — not the `.dock-btn` child. Dioxus's event delegation keys on `data-dioxus-id` walked from `event.target`, so the click bubbles out of the scroll container with no ancestor carrying the button handler, and the popup never opens. This is why `HTMLElement.click()` worked in the earlier probe: it synthesizes a click directly on the button, bypassing pointer capture. Real mouse / Playwright clicks go through the capture path and land on the wrong target. **Minimal fix:** arm `setPointerCapture` only when drag is actually confirmed. Move it into `pointermove` guarded by a small movement threshold (e.g. `Math.hypot(dx, dy) > 3`), and drop it from `pointerdown`. That preserves drag-to-scroll while leaving plain clicks alone. ```js el.addEventListener('pointerdown', function (e) { if (e.pointerType === 'touch') return; el.__heroDrag = { startX: e.clientX, startScroll: el.scrollLeft, id: e.pointerId, captured: false }; }); el.addEventListener('pointermove', function (e) { const d = el.__heroDrag; if (!d || e.pointerId !== d.id) return; const dx = e.clientX - d.startX; if (!d.captured && Math.abs(dx) > 3) { try { el.setPointerCapture(e.pointerId); } catch (err) {} el.dataset.dragging = 'true'; d.captured = true; } if (d.captured) { el.scrollLeft = d.startScroll - dx; e.preventDefault(); } }); ```
Author
Member

Fix verified in a local rebuild. Moving setPointerCapture out of pointerdown and into pointermove behind a 3px threshold restores the click path.

Playwright probe, same page as the original repro:

✓  dock click opens popup after fix (46.1s)
   panels after Projects click: 1
   child island buttons in popup: 6

Real mouse clicks on .dock-btn now route through Dioxus event delegation as before, and drag-to-scroll still works (capture is armed only once the pointer has moved past the threshold).

Fix verified in a local rebuild. Moving `setPointerCapture` out of `pointerdown` and into `pointermove` behind a 3px threshold restores the click path. Playwright probe, same page as the original repro: ``` ✓ dock click opens popup after fix (46.1s) panels after Projects click: 1 child island buttons in popup: 6 ``` Real mouse clicks on `.dock-btn` now route through Dioxus event delegation as before, and drag-to-scroll still works (capture is armed only once the pointer has moved past the threshold).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_os#79
No description provided.