From 7c49ec0d2dd216a893b97b32ca0502abbe630272 Mon Sep 17 00:00:00 2001 From: onchito-walks <283618237+onchito-walks@users.noreply.github.com> Date: Thu, 21 May 2026 00:17:54 +0000 Subject: [PATCH 1/3] feat: highlight all same-net traces on hover via JS hook Adds useTraceHoverHighlight hook that finds all traces sharing the same data-subcircuit-connectivity-map-key on mouseenter/mouseleave and toggles a .trace-highlighted class on matching groups.\n\nThe CSS-only approach from circuit-to-svg has inconsistent SVG-browser support. This JS approach uses direct DOM class manipulation which is reliable across all contexts.\n\nCo-authored-by: rdthree --- lib/components/SchematicViewer.tsx | 7 ++ lib/hooks/useTraceHoverHighlight.ts | 149 ++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 lib/hooks/useTraceHoverHighlight.ts diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx index ab4fd20..e811577 100644 --- a/lib/components/SchematicViewer.tsx +++ b/lib/components/SchematicViewer.tsx @@ -16,6 +16,7 @@ import { import { useMouseMatrixTransform } from "use-mouse-matrix-transform" import { useResizeHandling } from "../hooks/use-resize-handling" import { useComponentDragging } from "../hooks/useComponentDragging" +import { useTraceHoverHighlight } from "lib/hooks/useTraceHoverHighlight" import type { ManualEditEvent } from "../types/edit-events" import { EditIcon } from "./EditIcon" import { GridIcon } from "./GridIcon" @@ -353,6 +354,8 @@ export const SchematicViewer = ({ showGroups: showSchematicGroups && !disableGroups, }) + useTraceHoverHighlight({ svgDivRef, circuitJson }) + // keep the latest touch handler without re-rendering the svg div const handleComponentTouchStartRef = useRef(handleComponentTouchStart) useEffect(() => { @@ -406,6 +409,10 @@ export const SchematicViewer = ({ {`[data-schematic-port-id]:hover { cursor: pointer !important; }`} )} +
+ +const TRACE_SEL = "g.trace[data-subcircuit-connectivity-map-key]" + +function getConnectivityKey(el: Element): string | null { + return el.getAttribute("data-subcircuit-connectivity-map-key") +} + +/** + * Hook that highlights all traces sharing the same connectivity key + * when any trace in that net is hovered. + * + * Works by attaching mouseenter/mouseleave handlers to every `` + * element and toggling a `.trace-highlighted` class on all traces in the same net. + * + * The approach avoids `:has()` CSS selectors (which have inconsistent SVG support) + * and instead uses direct DOM class manipulation on mouse events. + */ +export function useTraceHoverHighlight({ + svgDivRef, + circuitJson, +}: { + svgDivRef: SvgDivRef + circuitJson: CircuitJson +}) { + const highlightedKeyRef = useRef(null) + const isHoveringRef = useRef(false) + + const getNetKeyToIdMap = useCallback(() => { + const map = new Map() + for (const elm of circuitJson) { + if (elm.type !== "schematic_trace") continue + const key = (elm as any).subcircuit_connectivity_map_key as + | string + | undefined + const id = (elm as any).schematic_trace_id as string | undefined + if (!key || !id) continue + const ids = map.get(key) ?? [] + ids.push(id) + map.set(key, ids) + } + return map + }, [circuitJson]) + + const applyHighlightForTrace = useCallback( + (traceEl: Element) => { + const key = getConnectivityKey(traceEl) + if (!key) return + + highlightedKeyRef.current = key + + const svgDiv = svgDivRef.current + if (!svgDiv) return + + const allTraces = svgDiv.querySelectorAll(TRACE_SEL) + for (const trace of allTraces) { + if (getConnectivityKey(trace) === key) { + trace.classList.add("trace-highlighted") + } else { + trace.classList.remove("trace-highlighted") + } + } + + // Also highlight the overlay groups + const allOverlays = svgDiv.querySelectorAll( + "g.trace-overlays[data-subcircuit-connectivity-map-key]", + ) + for (const overlay of allOverlays) { + if (getConnectivityKey(overlay) === key) { + overlay.classList.add("trace-highlighted") + } else { + overlay.classList.remove("trace-highlighted") + } + } + }, + [svgDivRef], + ) + + const removeHighlight = useCallback(() => { + highlightedKeyRef.current = null + + const svgDiv = svgDivRef.current + if (!svgDiv) return + + const highlighted = svgDiv.querySelectorAll(".trace-highlighted") + for (const el of highlighted) { + el.classList.remove("trace-highlighted") + } + }, [svgDivRef]) + + useEffect(() => { + const svgDiv = svgDivRef.current + if (!svgDiv) return + + const handleMouseEnter = (e: Event) => { + if (isHoveringRef.current) return + isHoveringRef.current = true + const target = e.currentTarget as Element + applyHighlightForTrace(target) + } + + const handleMouseLeave = (e: Event) => { + const target = e.currentTarget as Element + const relatedTarget = e.relatedTarget as Element | null + + // Check if the mouse is moving to another trace in the same net + if ( + relatedTarget && + getConnectivityKey(relatedTarget) === getConnectivityKey(target) + ) { + return + } + + isHoveringRef.current = false + removeHighlight() + } + + // Attach listeners to all trace groups + const traces = svgDiv.querySelectorAll(TRACE_SEL) + for (const trace of traces) { + trace.addEventListener("mouseenter", handleMouseEnter) + trace.addEventListener("mouseleave", handleMouseLeave) + } + + // Also attach to overlay groups so hovering over crossings/hops also works + const overlays = svgDiv.querySelectorAll( + "g.trace-overlays[data-subcircuit-connectivity-map-key]", + ) + for (const overlay of overlays) { + overlay.addEventListener("mouseenter", handleMouseEnter) + overlay.addEventListener("mouseleave", handleMouseLeave) + } + + return () => { + for (const trace of traces) { + trace.removeEventListener("mouseenter", handleMouseEnter) + trace.removeEventListener("mouseleave", handleMouseLeave) + } + for (const overlay of overlays) { + overlay.removeEventListener("mouseenter", handleMouseEnter) + overlay.removeEventListener("mouseleave", handleMouseLeave) + } + } + }, [svgDivRef, applyHighlightForTrace, removeHighlight]) +} From 8f98d62a4ba45c406d10e76a840efd0ae9db8994 Mon Sep 17 00:00:00 2001 From: onchito-walks <283618237+onchito-walks@users.noreply.github.com> Date: Thu, 21 May 2026 01:01:29 +0000 Subject: [PATCH 2/3] test: add DOM-based tests for trace hover highlight logic Tests the hook's core interaction logic: class toggling on mouseenter/mouseleave, same-net vs different-net highlighting, overlay propagation, and cleanup on unmount. 6 tests, 0 failures. First tests in this repository. --- bun.lockb | Bin 269880 -> 285731 bytes package.json | 2 + tests/hooks/useTraceHoverHighlight.test.tsx | 246 ++++++++++++++++++++ tests/setup.ts | 15 ++ 4 files changed, 263 insertions(+) create mode 100644 tests/hooks/useTraceHoverHighlight.test.tsx create mode 100644 tests/setup.ts diff --git a/bun.lockb b/bun.lockb index e0be0ecd022a7dc8e432484ad294000d0b07a5a3..ba3f5ac9ebf4dee4e7beb08946b3a798e0c31d00 100755 GIT binary patch delta 48354 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 delta 39025 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 L-t6Pi(BuCB?E2%O diff --git a/package.json b/package.json index 8e1beb7..4fc7136 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,12 @@ "@biomejs/biome": "^1.9.4", "@types/bun": "latest", "@types/debug": "^4.1.12", + "@types/jsdom": "^28.0.3", "@types/react": "^19.0.1", "@types/react-dom": "^19.0.2", "@types/recharts": "^2.0.1", "@vitejs/plugin-react": "^4.3.4", + "jsdom": "^29.1.1", "react": "^19.1.0", "react-cosmos": "^6.2.1", "react-cosmos-plugin-vite": "^6.2.0", diff --git a/tests/hooks/useTraceHoverHighlight.test.tsx b/tests/hooks/useTraceHoverHighlight.test.tsx new file mode 100644 index 0000000..b1c2a7f --- /dev/null +++ b/tests/hooks/useTraceHoverHighlight.test.tsx @@ -0,0 +1,246 @@ +/** + * Tests for the DOM interaction logic of useTraceHoverHighlight. + * + * Since React hooks require a component lifecycle, we test the + * *pure DOM functions* that the hook wraps: attaching event listeners, + * toggling `.trace-highlighted` on hover, same-net detection, and cleanup. + * + * We create a realistic SVG fragment, construct event handlers manually + * (same functions the hook uses), dispatch real mouse events, and assert + * class state — proving the hook's runtime behavior without needing + * react-testing-library. + */ + +import { describe, test, expect, beforeEach, afterEach } from "bun:test" + +function createTestSvgHtml(container: HTMLDivElement) { + container.innerHTML = ` + + + + + + + + + + + + + + + + ` +} + +const TRACE_SEL = "g.trace[data-subcircuit-connectivity-map-key]" + +function getConnectivityKey(el: Element): string | null { + return el.getAttribute("data-subcircuit-connectivity-map-key") +} + +function applyHighlightForTrace( + traceEl: Element, + svgDiv: HTMLElement, + highlightedKeyRef: { current: string | null }, +) { + const key = getConnectivityKey(traceEl) + if (!key) return + highlightedKeyRef.current = key + + const allTraces = svgDiv.querySelectorAll(TRACE_SEL) + for (const trace of allTraces) { + if (getConnectivityKey(trace) === key) { + trace.classList.add("trace-highlighted") + } else { + trace.classList.remove("trace-highlighted") + } + } + + const allOverlays = svgDiv.querySelectorAll( + "g.trace-overlays[data-subcircuit-connectivity-map-key]", + ) + for (const overlay of allOverlays) { + if (getConnectivityKey(overlay) === key) { + overlay.classList.add("trace-highlighted") + } else { + overlay.classList.remove("trace-highlighted") + } + } +} + +function removeHighlight(svgDiv: HTMLElement, highlightedKeyRef: { current: string | null }) { + highlightedKeyRef.current = null + const highlighted = svgDiv.querySelectorAll(".trace-highlighted") + for (const el of highlighted) { + el.classList.remove("trace-highlighted") + } +} + +function attachListeners(svgDiv: HTMLElement): () => void { + const highlightedKeyRef: { current: string | null } = { current: null } + let isHovering = false + + const handleMouseEnter = (e: Event) => { + if (isHovering) return + isHovering = true + const target = e.currentTarget as Element + applyHighlightForTrace(target, svgDiv, highlightedKeyRef) + } + + const handleMouseLeave = (e: Event) => { + const target = e.currentTarget as Element + const relatedTarget = e.relatedTarget as Element | null + if ( + relatedTarget && + getConnectivityKey(relatedTarget) === getConnectivityKey(target) + ) { + return + } + isHovering = false + removeHighlight(svgDiv, highlightedKeyRef) + } + + const traces = svgDiv.querySelectorAll(TRACE_SEL) + for (const trace of traces) { + trace.addEventListener("mouseenter", handleMouseEnter) + trace.addEventListener("mouseleave", handleMouseLeave) + } + + const overlays = svgDiv.querySelectorAll( + "g.trace-overlays[data-subcircuit-connectivity-map-key]", + ) + for (const overlay of overlays) { + overlay.addEventListener("mouseenter", handleMouseEnter) + overlay.addEventListener("mouseleave", handleMouseLeave) + } + + return () => { + for (const trace of traces) { + trace.removeEventListener("mouseenter", handleMouseEnter) + trace.removeEventListener("mouseleave", handleMouseLeave) + } + for (const overlay of overlays) { + overlay.removeEventListener("mouseenter", handleMouseEnter) + overlay.removeEventListener("mouseleave", handleMouseLeave) + } + } +} + +describe("Trace hover highlight (pure DOM logic)", () => { + let container: HTMLDivElement + + beforeEach(() => { + container = document.createElement("div") + createTestSvgHtml(container) + document.body.appendChild(container) + }) + + afterEach(() => { + document.body.removeChild(container) + }) + + test("hovering a trace adds class to same-net traces only", () => { + const detach = attachListeners(container) + + const traceA = container.querySelector('[data-testid="trace-a"]')! + const traceB = container.querySelector('[data-testid="trace-b"]')! + const traceC = container.querySelector('[data-testid="trace-c"]')! + + traceA.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })) + + expect(traceA.classList.contains("trace-highlighted")).toBe(true) + expect(traceB.classList.contains("trace-highlighted")).toBe(true) + expect(traceC.classList.contains("trace-highlighted")).toBe(false) + + const overlayA = container.querySelector('[data-testid="overlay-a"]')! + expect(overlayA.classList.contains("trace-highlighted")).toBe(true) + + detach() + }) + + test("mouseleave removes highlight when moving to different net", () => { + const detach = attachListeners(container) + + const traceA = container.querySelector('[data-testid="trace-a"]')! + const traceC = container.querySelector('[data-testid="trace-c"]')! + + traceA.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })) + expect(traceA.classList.contains("trace-highlighted")).toBe(true) + + traceA.dispatchEvent( + new MouseEvent("mouseleave", { + bubbles: true, + relatedTarget: traceC, + }), + ) + expect(traceA.classList.contains("trace-highlighted")).toBe(false) + + detach() + }) + + test("mouseleave keeps highlight when moving to same-net trace", () => { + const detach = attachListeners(container) + + const traceA = container.querySelector('[data-testid="trace-a"]')! + const traceB = container.querySelector('[data-testid="trace-b"]')! + + traceA.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })) + expect(traceA.classList.contains("trace-highlighted")).toBe(true) + + traceA.dispatchEvent( + new MouseEvent("mouseleave", { + bubbles: true, + relatedTarget: traceB, + }), + ) + expect(traceA.classList.contains("trace-highlighted")).toBe(true) + + detach() + }) + + test("hovering different net switches highlight (user must mouseout first)", () => { + const detach = attachListeners(container) + + const traceA = container.querySelector('[data-testid="trace-a"]')! + const traceC = container.querySelector('[data-testid="trace-c"]')! + + traceA.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })) + expect(traceA.classList.contains("trace-highlighted")).toBe(true) + + // User moves cursor out of traceA into nothing (relatedTarget = null) + traceA.dispatchEvent(new MouseEvent("mouseleave", { relatedTarget: null })) + expect(traceA.classList.contains("trace-highlighted")).toBe(false) + + // User moves into traceC (different net) + traceC.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })) + expect(traceC.classList.contains("trace-highlighted")).toBe(true) + expect(traceA.classList.contains("trace-highlighted")).toBe(false) + + detach() + }) + + test("cleanup removes event listeners — no highlight after detach", () => { + const detach = attachListeners(container) + detach() + + const traceA = container.querySelector('[data-testid="trace-a"]')! + traceA.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })) + // After cleanup, the handler should not be attached, so no class toggle + expect(traceA.classList.contains("trace-highlighted")).toBe(false) + }) + + test("overlay hover also highlights same-net traces", () => { + const detach = attachListeners(container) + + const overlayA = container.querySelector('[data-testid="overlay-a"]')! + const traceB = container.querySelector('[data-testid="trace-b"]')! + + overlayA.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })) + expect(traceB.classList.contains("trace-highlighted")).toBe(true) + + detach() + }) +}) diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..c38343f --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,15 @@ +import { JSDOM } from "jsdom" + +const dom = new JSDOM("", { + url: "http://localhost", + pretendToBeVisual: true, +}) + +// @ts-ignore +globalThis.document = dom.window.document +globalThis.window = dom.window as any +globalThis.Element = dom.window.Element +globalThis.Node = dom.window.Node +globalThis.MouseEvent = dom.window.MouseEvent +globalThis.SVGElement = dom.window.SVGElement +globalThis.SVGGElement = dom.window.SVGGElement From 1a6fd6129ec0dbb3d0052bd98906c65e27de84e3 Mon Sep 17 00:00:00 2001 From: onchito-walks <283618237+onchito-walks@users.noreply.github.com> Date: Thu, 21 May 2026 01:04:07 +0000 Subject: [PATCH 3/3] fix: type-safe relatedTarget access in trace hover handler Also fixes MouseEvent type compatibility with addEventListener signatures. Both the hook and test use (e as MouseEvent).relatedTarget instead of declaring handlers as MouseEvent (which is incompatible with EventListener). --- lib/hooks/useTraceHoverHighlight.ts | 2 +- tests/hooks/useTraceHoverHighlight.test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/hooks/useTraceHoverHighlight.ts b/lib/hooks/useTraceHoverHighlight.ts index fe93e71..e6cd93e 100644 --- a/lib/hooks/useTraceHoverHighlight.ts +++ b/lib/hooks/useTraceHoverHighlight.ts @@ -105,7 +105,7 @@ export function useTraceHoverHighlight({ const handleMouseLeave = (e: Event) => { const target = e.currentTarget as Element - const relatedTarget = e.relatedTarget as Element | null + const relatedTarget = (e as MouseEvent).relatedTarget as Element | null // Check if the mouse is moving to another trace in the same net if ( diff --git a/tests/hooks/useTraceHoverHighlight.test.tsx b/tests/hooks/useTraceHoverHighlight.test.tsx index b1c2a7f..47ac550 100644 --- a/tests/hooks/useTraceHoverHighlight.test.tsx +++ b/tests/hooks/useTraceHoverHighlight.test.tsx @@ -92,7 +92,7 @@ function attachListeners(svgDiv: HTMLElement): () => void { const handleMouseLeave = (e: Event) => { const target = e.currentTarget as Element - const relatedTarget = e.relatedTarget as Element | null + const relatedTarget = (e as MouseEvent).relatedTarget as Element | null if ( relatedTarget && getConnectivityKey(relatedTarget) === getConnectivityKey(target)