From 5bc64a5b4ebd2e5e628499868e35f75465a4dc81 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Tue, 31 Mar 2026 11:06:44 -0700 Subject: [PATCH] fix(touch): pointer event parity for slash commands, collab overlay, canvas, and wallet charts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert remaining mouse-only event listeners to Pointer Events so touch and pen inputs work identically to mouse across rApps and canvas: - rnotes slash-command: mousedown/mouseenter → pointerdown/pointerenter - collab overlay: mousemove → pointermove for cursor broadcast, click → pointerdown for collab-id tracking - canvas.html: toolbar label reveal gated behind @media (hover: hover), :active fallbacks on 12 button selectors, JS mouseenter/mouseleave → pointerenter/pointerleave - wallet-viz D3 charts: all mouseover/mousemove/mouseout → pointerenter/pointermove/pointerleave on transaction paths, balance area, Sankey links, and reset button Co-Authored-By: Claude Opus 4.6 --- modules/rnotes/components/slash-command.ts | 4 +-- modules/rwallet/lib/wallet-viz.ts | 24 +++++++------- shared/components/rstack-collab-overlay.ts | 16 ++++----- website/canvas.html | 38 ++++++++++++---------- 4 files changed, 43 insertions(+), 39 deletions(-) diff --git a/modules/rnotes/components/slash-command.ts b/modules/rnotes/components/slash-command.ts index 774569c..d751e57 100644 --- a/modules/rnotes/components/slash-command.ts +++ b/modules/rnotes/components/slash-command.ts @@ -172,12 +172,12 @@ export function createSlashCommandPlugin(editor: Editor, shadowRoot: ShadowRoot) // Click handlers menuEl.querySelectorAll('.slash-menu-item').forEach((el) => { - el.addEventListener('mousedown', (e) => { + el.addEventListener('pointerdown', (e) => { e.preventDefault(); const idx = parseInt((el as HTMLElement).dataset.index || '0'); executeItem(idx); }); - el.addEventListener('mouseenter', () => { + el.addEventListener('pointerenter', () => { selectedIndex = parseInt((el as HTMLElement).dataset.index || '0'); updateMenuContent(); }); diff --git a/modules/rwallet/lib/wallet-viz.ts b/modules/rwallet/lib/wallet-viz.ts index 86ba8f9..1143c5a 100644 --- a/modules/rwallet/lib/wallet-viz.ts +++ b/modules/rwallet/lib/wallet-viz.ts @@ -70,8 +70,8 @@ function addResetButton(parent: HTMLElement, svg: any, zoom: any): void { border: "1px solid rgba(255,255,255,0.15)", borderRadius: "6px", cursor: "pointer", backdropFilter: "blur(4px)", }); - btn.addEventListener("mouseenter", () => { btn.style.background = "rgba(255,255,255,0.15)"; }); - btn.addEventListener("mouseleave", () => { btn.style.background = "rgba(255,255,255,0.08)"; }); + btn.addEventListener("pointerenter", () => { btn.style.background = "rgba(255,255,255,0.15)"; }); + btn.addEventListener("pointerleave", () => { btn.style.background = "rgba(255,255,255,0.08)"; }); btn.addEventListener("click", () => { svg.transition().duration(500).call(zoom.transform, d3.zoomIdentity); }); @@ -293,9 +293,9 @@ export function renderTimeline( contentGroup.append("path").attr("d", path.toString()).attr("fill", `url(#${id}-inflow)`) .attr("opacity", 0.85).style("cursor", "pointer").style("transition", "opacity 0.2s, filter 0.2s") - .on("mouseover", (event: any) => showTxTooltip(event, tx, prevBalance)) - .on("mousemove", (event: any) => moveTip(event)) - .on("mouseout", () => { tooltip.style.display = "none"; }); + .on("pointerenter", (event: any) => showTxTooltip(event, tx, prevBalance)) + .on("pointermove", (event: any) => moveTip(event)) + .on("pointerleave", () => { tooltip.style.display = "none"; }); } else { const startY = riverBottomBefore; const endY = startY + flowHeight; @@ -318,9 +318,9 @@ export function renderTimeline( contentGroup.append("path").attr("d", path.toString()).attr("fill", `url(#${id}-outflow)`) .attr("opacity", 0.85).style("cursor", "pointer").style("transition", "opacity 0.2s, filter 0.2s") - .on("mouseover", (event: any) => showTxTooltip(event, tx, prevBalance)) - .on("mousemove", (event: any) => moveTip(event)) - .on("mouseout", () => { tooltip.style.display = "none"; }); + .on("pointerenter", (event: any) => showTxTooltip(event, tx, prevBalance)) + .on("pointermove", (event: any) => moveTip(event)) + .on("pointerleave", () => { tooltip.style.display = "none"; }); } }); @@ -330,7 +330,7 @@ export function renderTimeline( .attr("x", scale.range()[0] - 50).attr("y", centerY - 100) .attr("width", scale.range()[1] - scale.range()[0] + 100).attr("height", 200) .attr("fill", "transparent").style("cursor", "crosshair") - .on("mousemove", function(event: any) { + .on("pointermove", function(event: any) { const [mouseX] = d3.pointer(event); const hoveredDate = scale.invert(mouseX); let balAtPoint = 0; @@ -357,7 +357,7 @@ export function renderTimeline( tooltip.style.display = "block"; moveTip(event); }) - .on("mouseout", function() { + .on("pointerleave", function() { riverHoverGroup.selectAll(".bal-ind").remove(); tooltip.style.display = "none"; }); @@ -627,8 +627,8 @@ export function renderSankey( .attr("stroke-opacity", 0.4) .attr("stroke-width", (d: any) => Math.max(1, d.width)) .style("transition", "stroke-opacity 0.2s") - .on("mouseover", function(this: any) { d3.select(this).attr("stroke-opacity", 0.7); }) - .on("mouseout", function(this: any) { d3.select(this).attr("stroke-opacity", 0.4); }) + .on("pointerenter", function(this: any) { d3.select(this).attr("stroke-opacity", 0.7); }) + .on("pointerleave", function(this: any) { d3.select(this).attr("stroke-opacity", 0.4); }) .append("title") .text((d: any) => `${d.source.name} -> ${d.target.name}\n${d.value.toLocaleString()} ${d.token}`); diff --git a/shared/components/rstack-collab-overlay.ts b/shared/components/rstack-collab-overlay.ts index 3152ffd..b187eb6 100644 --- a/shared/components/rstack-collab-overlay.ts +++ b/shared/components/rstack-collab-overlay.ts @@ -392,14 +392,14 @@ export class RStackCollabOverlay extends HTMLElement { }); } - // ── Mouse tracking (15Hz throttle) ── + // ── Pointer tracking (15Hz throttle) — covers mouse, touch, and pen ── - #mouseHandler = (e: MouseEvent) => { + #pointerHandler = (e: PointerEvent) => { this.#lastCursor = { x: e.clientX, y: e.clientY }; }; #startMouseTracking() { - document.addEventListener('mousemove', this.#mouseHandler, { passive: true }); + document.addEventListener('pointermove', this.#pointerHandler, { passive: true }); // Broadcast at 15Hz this.#mouseMoveTimer = setInterval(() => { this.#broadcastPresence(this.#lastCursor, undefined); @@ -407,7 +407,7 @@ export class RStackCollabOverlay extends HTMLElement { } #stopMouseTracking() { - document.removeEventListener('mousemove', this.#mouseHandler); + document.removeEventListener('pointermove', this.#pointerHandler); if (this.#mouseMoveTimer) clearInterval(this.#mouseMoveTimer); this.#mouseMoveTimer = null; } @@ -429,18 +429,18 @@ export class RStackCollabOverlay extends HTMLElement { #startFocusTracking() { document.addEventListener('focusin', this.#focusHandler, { passive: true }); - document.addEventListener('click', this.#clickHandler, { passive: true }); + document.addEventListener('pointerdown', this.#clickHandler, { passive: true }); document.addEventListener('focusout', this.#blurHandler, { passive: true }); } #stopFocusTracking() { document.removeEventListener('focusin', this.#focusHandler); - document.removeEventListener('click', this.#clickHandler); + document.removeEventListener('pointerdown', this.#clickHandler); document.removeEventListener('focusout', this.#blurHandler); } - // Also track clicks on data-collab-id (many elements aren't focusable) - #clickHandler = (e: MouseEvent) => { + // Also track taps/clicks on data-collab-id (many elements aren't focusable) + #clickHandler = (e: PointerEvent) => { const real = (e.composedPath()[0] as HTMLElement); const target = real?.closest?.('[data-collab-id]'); if (target) { diff --git a/website/canvas.html b/website/canvas.html index a82fcad..2b21617 100644 --- a/website/canvas.html +++ b/website/canvas.html @@ -83,7 +83,7 @@ justify-content: center; } - .toolbar-group-toggle:hover { + .toolbar-group-toggle:hover, .toolbar-group-toggle:active { background: var(--rs-toolbar-btn-hover); } @@ -136,7 +136,7 @@ touch-action: manipulation; } - .toolbar-dropdown button:hover { + .toolbar-dropdown button:hover, .toolbar-dropdown button:active { background: var(--rs-toolbar-btn-bg); } @@ -281,7 +281,7 @@ touch-action: manipulation; } - #toolbar > button:hover { + #toolbar > button:hover, #toolbar > button:active { background: var(--rs-toolbar-btn-hover); } @@ -321,11 +321,15 @@ position: relative; } - /* Show label outside toolbar on hover or when group is open */ - .toolbar-group:hover > .toolbar-group-toggle .tg-label, + /* Show label outside toolbar on hover (only on hover-capable devices) or when group is open */ .toolbar-group.open > .toolbar-group-toggle .tg-label { display: block; } + @media (hover: hover) { + .toolbar-group:hover > .toolbar-group-toggle .tg-label { + display: block; + } + } /* Collapse/expand toggle — at bottom of toolbar */ #toolbar-collapse { @@ -347,7 +351,7 @@ justify-content: center; } - #toolbar-collapse:hover { + #toolbar-collapse:hover, #toolbar-collapse:active { opacity: 1; background: var(--rs-toolbar-btn-bg); color: var(--rs-text-primary); @@ -401,7 +405,7 @@ position: relative; } - #bottom-toolbar .tool-btn:hover { + #bottom-toolbar .tool-btn:hover, #bottom-toolbar .tool-btn:active { background: var(--rs-toolbar-btn-hover); } @@ -449,7 +453,7 @@ opacity: 0.7; } - #recent-tools .recent-tool-btn:hover { + #recent-tools .recent-tool-btn:hover, #recent-tools .recent-tool-btn:active { background: var(--rs-toolbar-btn-hover); opacity: 1; } @@ -506,7 +510,7 @@ transition: background 0.15s; } - #tool-plus-menu button:hover { + #tool-plus-menu button:hover, #tool-plus-menu button:active { background: var(--rs-toolbar-btn-hover); } @@ -563,7 +567,7 @@ transition: background 0.15s; } - #arrow-style-menu button:hover { + #arrow-style-menu button:hover, #arrow-style-menu button:active { background: var(--rs-toolbar-btn-hover); } @@ -642,7 +646,7 @@ touch-action: manipulation; } - .corner-btn:hover { + .corner-btn:hover, .corner-btn:active { background: var(--rs-toolbar-btn-hover); } @@ -886,7 +890,7 @@ cursor: pointer; } - #shape-context-menu button:hover { + #shape-context-menu button:hover, #shape-context-menu button:active { background: var(--rs-bg-hover); } @@ -894,7 +898,7 @@ color: var(--rs-error); } - #shape-context-menu button.danger:hover { + #shape-context-menu button.danger:hover, #shape-context-menu button.danger:active { background: rgba(239, 68, 68, 0.1); } @@ -935,7 +939,7 @@ cursor: pointer; } - .submenu button:hover { + .submenu button:hover, .submenu button:active { background: var(--rs-bg-hover); } @@ -5949,10 +5953,10 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest menu.innerHTML = items; document.body.appendChild(menu); - // Hover effect + // Hover/press effect menu.querySelectorAll("button").forEach(b => { - b.addEventListener("mouseenter", () => b.style.background = "#f1f5f9"); - b.addEventListener("mouseleave", () => b.style.background = "none"); + b.addEventListener("pointerenter", () => b.style.background = "#f1f5f9"); + b.addEventListener("pointerleave", () => b.style.background = "none"); }); const closeMenu = (ev) => {