From 7d3f231ab3e872346c94b845ca29af03f0ace855 Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Mon, 14 Jul 2025 17:52:57 -0700 Subject: [PATCH 1/2] feat: add schematic port hover tooltip --- lib/components/SchematicViewer.tsx | 74 ++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx index 1e9a2c8..1344b03 100644 --- a/lib/components/SchematicViewer.tsx +++ b/lib/components/SchematicViewer.tsx @@ -6,6 +6,7 @@ import { useChangeSchematicComponentLocationsInSvg } from "lib/hooks/useChangeSc import { useChangeSchematicTracesForMovedComponents } from "lib/hooks/useChangeSchematicTracesForMovedComponents" import { enableDebug } from "lib/utils/debug" import { useEffect, useMemo, useRef, useState } from "react" +import { su } from "@tscircuit/soup-util" import { fromString, identity, @@ -54,6 +55,11 @@ export const SchematicViewer = ({ !clickToInteractEnabled, ) const svgDivRef = useRef(null) + const [hoverLabel, setHoverLabel] = useState<{ + name: string + x: number + y: number + } | null>(null) const touchStartRef = useRef<{ x: number; y: number } | null>(null) const handleTouchStart = (e: React.TouchEvent) => { @@ -189,6 +195,46 @@ export const SchematicViewer = ({ editEvents: editEventsWithUnappliedEditEvents, }) + useEffect(() => { + const svg = svgDivRef.current + if (!svg) return + + const container = containerRef.current + if (!container) return + + const handleEnter = (e: Event) => { + const target = e.currentTarget as SVGGElement + const id = target.getAttribute("data-schematic-port-id") + if (!id) return + const port = su(circuitJson).source_port.get(id as any) + const name = (port as any)?.name || id + const ev = e as MouseEvent + setHoverLabel({ name, x: ev.clientX, y: ev.clientY }) + } + const handleMove = (e: Event) => { + const ev = e as MouseEvent + setHoverLabel((prev) => + prev ? { ...prev, x: ev.clientX, y: ev.clientY } : prev, + ) + } + const handleLeave = () => setHoverLabel(null) + + const portEls = svg.querySelectorAll(".schematic-port-hover") + portEls.forEach((el) => { + el.addEventListener("mouseenter", handleEnter) + el.addEventListener("mousemove", handleMove) + el.addEventListener("mouseleave", handleLeave) + }) + + return () => { + portEls.forEach((el) => { + el.removeEventListener("mouseenter", handleEnter) + el.removeEventListener("mousemove", handleMove) + el.removeEventListener("mouseleave", handleLeave) + }) + } + }, [svgString, circuitJson]) + const svgDiv = useMemo( () => (
{ + if (!hoverLabel) return null + const rect = containerRef.current?.getBoundingClientRect() + if (!rect) return null + const left = hoverLabel.x - rect.left + 10 + const top = hoverLabel.y - rect.top + 10 + return ( +
+ {hoverLabel.name} +
+ ) + }, [hoverLabel]) + return (
)} {svgDiv} + {hoverLabelDiv}
) } From c21d2bb6777065c1194cdc38118b039e45668075 Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Tue, 15 Jul 2025 14:07:49 -0700 Subject: [PATCH 2/2] refactor: extract port hover into hook and tooltip --- lib/components/SchematicPortHoverTooltip.tsx | 36 ++++++++ lib/components/SchematicViewer.tsx | 86 +++----------------- lib/hooks/useSchematicPortHover.ts | 62 ++++++++++++++ lib/utils/z-index-map.ts | 1 + 4 files changed, 111 insertions(+), 74 deletions(-) create mode 100644 lib/components/SchematicPortHoverTooltip.tsx create mode 100644 lib/hooks/useSchematicPortHover.ts diff --git a/lib/components/SchematicPortHoverTooltip.tsx b/lib/components/SchematicPortHoverTooltip.tsx new file mode 100644 index 0000000..eda726d --- /dev/null +++ b/lib/components/SchematicPortHoverTooltip.tsx @@ -0,0 +1,36 @@ +import React from "react" +import type { HoverLabel } from "../hooks/useSchematicPortHover" +import { zIndexMap } from "../utils/z-index-map" + +export const SchematicPortHoverTooltip = ({ + containerRef, + hoverLabel, +}: { + containerRef: React.RefObject + hoverLabel: HoverLabel | null +}) => { + if (!hoverLabel) return null + const rect = containerRef.current?.getBoundingClientRect() + if (!rect) return null + const left = hoverLabel.x - rect.left + 10 + const top = hoverLabel.y - rect.top + 10 + return ( +
+ {hoverLabel.name} +
+ ) +} diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx index 1344b03..773aa90 100644 --- a/lib/components/SchematicViewer.tsx +++ b/lib/components/SchematicViewer.tsx @@ -6,7 +6,6 @@ import { useChangeSchematicComponentLocationsInSvg } from "lib/hooks/useChangeSc import { useChangeSchematicTracesForMovedComponents } from "lib/hooks/useChangeSchematicTracesForMovedComponents" import { enableDebug } from "lib/utils/debug" import { useEffect, useMemo, useRef, useState } from "react" -import { su } from "@tscircuit/soup-util" import { fromString, identity, @@ -20,6 +19,8 @@ import { EditIcon } from "./EditIcon" import { GridIcon } from "./GridIcon" import type { CircuitJson } from "circuit-json" import { zIndexMap } from "../utils/z-index-map" +import { useSchematicPortHover } from "../hooks/useSchematicPortHover" +import { SchematicPortHoverTooltip } from "./SchematicPortHoverTooltip" interface Props { circuitJson: CircuitJson @@ -55,11 +56,6 @@ export const SchematicViewer = ({ !clickToInteractEnabled, ) const svgDivRef = useRef(null) - const [hoverLabel, setHoverLabel] = useState<{ - name: string - x: number - y: number - } | null>(null) const touchStartRef = useRef<{ x: number; y: number } | null>(null) const handleTouchStart = (e: React.TouchEvent) => { @@ -135,6 +131,12 @@ export const SchematicViewer = ({ }) }, [circuitJson, containerWidth, containerHeight]) + const { hoverLabel } = useSchematicPortHover({ + svgDivRef, + circuitJson, + svgString, + }) + const containerBackgroundColor = useMemo(() => { const match = svgString.match( /]*style="[^"]*background-color:\s*([^;\"]+)/i, @@ -195,46 +197,6 @@ export const SchematicViewer = ({ editEvents: editEventsWithUnappliedEditEvents, }) - useEffect(() => { - const svg = svgDivRef.current - if (!svg) return - - const container = containerRef.current - if (!container) return - - const handleEnter = (e: Event) => { - const target = e.currentTarget as SVGGElement - const id = target.getAttribute("data-schematic-port-id") - if (!id) return - const port = su(circuitJson).source_port.get(id as any) - const name = (port as any)?.name || id - const ev = e as MouseEvent - setHoverLabel({ name, x: ev.clientX, y: ev.clientY }) - } - const handleMove = (e: Event) => { - const ev = e as MouseEvent - setHoverLabel((prev) => - prev ? { ...prev, x: ev.clientX, y: ev.clientY } : prev, - ) - } - const handleLeave = () => setHoverLabel(null) - - const portEls = svg.querySelectorAll(".schematic-port-hover") - portEls.forEach((el) => { - el.addEventListener("mouseenter", handleEnter) - el.addEventListener("mousemove", handleMove) - el.addEventListener("mouseleave", handleLeave) - }) - - return () => { - portEls.forEach((el) => { - el.removeEventListener("mouseenter", handleEnter) - el.removeEventListener("mousemove", handleMove) - el.removeEventListener("mouseleave", handleLeave) - }) - } - }, [svgString, circuitJson]) - const svgDiv = useMemo( () => (
{ - if (!hoverLabel) return null - const rect = containerRef.current?.getBoundingClientRect() - if (!rect) return null - const left = hoverLabel.x - rect.left + 10 - const top = hoverLabel.y - rect.top + 10 - return ( -
- {hoverLabel.name} -
- ) - }, [hoverLabel]) - return (
)} {svgDiv} - {hoverLabelDiv} +
) } diff --git a/lib/hooks/useSchematicPortHover.ts b/lib/hooks/useSchematicPortHover.ts new file mode 100644 index 0000000..f540a98 --- /dev/null +++ b/lib/hooks/useSchematicPortHover.ts @@ -0,0 +1,62 @@ +import { useEffect, useState } from "react" +import { su } from "@tscircuit/soup-util" +import type { CircuitJson } from "circuit-json" + +export interface HoverLabel { + name: string + x: number + y: number +} + +export const useSchematicPortHover = ({ + svgDivRef, + circuitJson, + svgString, +}: { + svgDivRef: React.RefObject + circuitJson: CircuitJson + svgString: string +}) => { + const [hoverLabel, setHoverLabel] = useState(null) + + useEffect(() => { + const svg = svgDivRef.current + if (!svg) return + + const handleEnter = (e: Event) => { + const target = e.currentTarget as SVGGElement + const id = target.getAttribute("data-schematic-port-id") + if (!id) return + const port = su(circuitJson).source_port.get(id as any) + const name = (port as any)?.name || id + const ev = e as MouseEvent + setHoverLabel({ name, x: ev.clientX, y: ev.clientY }) + } + + const handleMove = (e: Event) => { + const ev = e as MouseEvent + setHoverLabel((prev) => + prev ? { ...prev, x: ev.clientX, y: ev.clientY } : prev, + ) + } + + const handleLeave = () => setHoverLabel(null) + + const portEls = svg.querySelectorAll(".schematic-port-hover") + portEls.forEach((el) => { + el.addEventListener("mouseenter", handleEnter) + el.addEventListener("mousemove", handleMove) + el.addEventListener("mouseleave", handleLeave) + }) + + return () => { + portEls.forEach((el) => { + el.removeEventListener("mouseenter", handleEnter) + el.removeEventListener("mousemove", handleMove) + el.removeEventListener("mouseleave", handleLeave) + }) + } + }, [svgString, circuitJson]) + + return { hoverLabel } +} diff --git a/lib/utils/z-index-map.ts b/lib/utils/z-index-map.ts index 891151e..52e01b6 100644 --- a/lib/utils/z-index-map.ts +++ b/lib/utils/z-index-map.ts @@ -2,4 +2,5 @@ export const zIndexMap = { schematicEditIcon: 50, schematicGridIcon: 49, clickToInteractOverlay: 100, + schematicPortHoverLabel: 200, }