Skip to content

Commit 106856d

Browse files
authored
Fix hover labels rendering above shapes (#114)
* Fix hover label stacking * Render interactive hover tooltips in a top-level overlay
1 parent 3e8c8ec commit 106856d

10 files changed

Lines changed: 225 additions & 137 deletions

File tree

site/components/InteractiveGraphics/Arrow.tsx

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { getArrowGeometry, getInlineLabelLayout } from "lib/arrowHelpers"
22
import type * as Types from "lib/types"
3-
import { useMemo, useState } from "react"
3+
import { useEffect, useMemo, useState } from "react"
44
import { distToLineSegment } from "site/utils/distToLineSegment"
55
import { safeLighten } from "site/utils/safeLighten"
66
import { applyToPoint } from "transformation-matrix"
77
import type { InteractiveState } from "./InteractiveState"
88
import { defaultColors } from "./defaultColors"
9-
import { Tooltip } from "./Tooltip"
109

1110
export const Arrow = ({
1211
arrow,
@@ -17,7 +16,7 @@ export const Arrow = ({
1716
index: number
1817
interactiveState: InteractiveState
1918
}) => {
20-
const { realToScreen, onObjectClicked } = interactiveState
19+
const { realToScreen, onObjectClicked, setHoverTooltip } = interactiveState
2120
const [isHovered, setIsHovered] = useState(false)
2221

2322
const geometry = useMemo(() => getArrowGeometry(arrow), [arrow])
@@ -68,6 +67,26 @@ export const Arrow = ({
6867
.join("\n")
6968
const tooltipAnchor = arrow.label ? labelLayout : inlineLabelLayout
7069

70+
useEffect(() => {
71+
if (!isHovered || !tooltipText) return
72+
73+
setHoverTooltip?.({
74+
text: tooltipText,
75+
x: tooltipAnchor.x,
76+
y: tooltipAnchor.y,
77+
})
78+
79+
return () => {
80+
setHoverTooltip?.(null)
81+
}
82+
}, [
83+
isHovered,
84+
setHoverTooltip,
85+
tooltipAnchor.x,
86+
tooltipAnchor.y,
87+
tooltipText,
88+
])
89+
7190
const handleMouseMove = (e: React.MouseEvent<SVGSVGElement>) => {
7291
const rect = e.currentTarget.getBoundingClientRect()
7392
const mouseX = e.clientX - rect.left
@@ -119,7 +138,10 @@ export const Arrow = ({
119138
pointerEvents: "auto",
120139
}}
121140
onMouseMove={handleMouseMove}
122-
onMouseLeave={() => setIsHovered(false)}
141+
onMouseLeave={() => {
142+
setIsHovered(false)
143+
setHoverTooltip?.(null)
144+
}}
123145
onClick={
124146
isHovered
125147
? (event) => {
@@ -147,19 +169,6 @@ export const Arrow = ({
147169
/>
148170
))}
149171
</svg>
150-
{isHovered && tooltipText && (
151-
<div
152-
style={{
153-
position: "absolute",
154-
left: tooltipAnchor.x,
155-
top: tooltipAnchor.y - 8,
156-
transform: "translate(-50%, -100%)",
157-
pointerEvents: "none",
158-
}}
159-
>
160-
<Tooltip text={tooltipText} />
161-
</div>
162-
)}
163172
</div>
164173
)
165174
}
Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import type * as Types from "lib/types"
22
import { applyToPoint } from "transformation-matrix"
33
import type { InteractiveState } from "./InteractiveState"
4-
import { useState } from "react"
5-
import { Tooltip } from "./Tooltip"
4+
import { useEffect, useState } from "react"
65
import { defaultColors } from "./defaultColors"
76
import { safeLighten } from "site/utils/safeLighten"
87

@@ -17,8 +16,13 @@ export const Circle = ({
1716
}) => {
1817
const defaultColor = defaultColors[index % defaultColors.length]
1918
let { center, radius, fill, stroke, layer, step, label } = circle
20-
const { activeLayers, activeStep, realToScreen, onObjectClicked } =
21-
interactiveState
19+
const {
20+
activeLayers,
21+
activeStep,
22+
realToScreen,
23+
onObjectClicked,
24+
setHoverTooltip,
25+
} = interactiveState
2226
const [isHovered, setIsHovered] = useState(false)
2327
const screenCenter = applyToPoint(realToScreen, center)
2428
const screenRadius = radius * realToScreen.a
@@ -27,6 +31,28 @@ export const Circle = ({
2731
backgroundColor = safeLighten(0.2, backgroundColor)
2832
stroke = stroke ? safeLighten(0.2, stroke) : stroke
2933
}
34+
35+
useEffect(() => {
36+
if (!isHovered || !label) return
37+
38+
setHoverTooltip?.({
39+
text: label,
40+
x: screenCenter.x,
41+
y: screenCenter.y - screenRadius,
42+
})
43+
44+
return () => {
45+
setHoverTooltip?.(null)
46+
}
47+
}, [
48+
isHovered,
49+
label,
50+
screenCenter.x,
51+
screenCenter.y,
52+
screenRadius,
53+
setHoverTooltip,
54+
])
55+
3056
return (
3157
<div
3258
style={{
@@ -42,28 +68,17 @@ export const Circle = ({
4268
transition: "border-color 0.2s",
4369
}}
4470
onMouseEnter={() => setIsHovered(true)}
45-
onMouseLeave={() => setIsHovered(false)}
71+
onMouseLeave={() => {
72+
setIsHovered(false)
73+
setHoverTooltip?.(null)
74+
}}
4675
onClick={() =>
4776
onObjectClicked?.({
4877
type: "circle",
4978
index,
5079
object: circle,
5180
})
5281
}
53-
>
54-
{isHovered && label && (
55-
<div
56-
style={{
57-
position: "absolute",
58-
bottom: "100%",
59-
left: "50%",
60-
transform: "translateX(-50%)",
61-
marginBottom: 8,
62-
}}
63-
>
64-
<Tooltip text={label} />
65-
</div>
66-
)}
67-
</div>
82+
></div>
6883
)
6984
}

site/components/InteractiveGraphics/InfiniteLine.tsx

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { safeLighten } from "site/utils/safeLighten"
88
import { applyToPoint } from "transformation-matrix"
99
import type { InteractiveState } from "./InteractiveState"
1010
import { defaultColors } from "./defaultColors"
11-
import { Tooltip } from "./Tooltip"
1211

1312
export const InfiniteLine = ({
1413
infiniteLine,
@@ -21,7 +20,7 @@ export const InfiniteLine = ({
2120
interactiveState: InteractiveState
2221
size: { width: number; height: number }
2322
}) => {
24-
const { realToScreen, onObjectClicked } = interactiveState
23+
const { realToScreen, onObjectClicked, setHoverTooltip } = interactiveState
2524
const [isHovered, setIsHovered] = useState(false)
2625

2726
const viewportBounds = getViewportBoundsFromMatrix(
@@ -72,8 +71,20 @@ export const InfiniteLine = ({
7271
stroke="transparent"
7372
strokeWidth={strokeWidth + 10}
7473
pointerEvents="stroke"
75-
onMouseEnter={() => setIsHovered(true)}
76-
onMouseLeave={() => setIsHovered(false)}
74+
onMouseEnter={() => {
75+
setIsHovered(true)
76+
if (infiniteLine.label) {
77+
setHoverTooltip?.({
78+
text: infiniteLine.label,
79+
x: tooltipX,
80+
y: tooltipY,
81+
})
82+
}
83+
}}
84+
onMouseLeave={() => {
85+
setIsHovered(false)
86+
setHoverTooltip?.(null)
87+
}}
7788
onClick={() =>
7889
onObjectClicked?.({
7990
type: "infinite-line",
@@ -101,19 +112,6 @@ export const InfiniteLine = ({
101112
pointerEvents="none"
102113
/>
103114
</svg>
104-
{isHovered && infiniteLine.label && (
105-
<div
106-
style={{
107-
position: "absolute",
108-
left: tooltipX,
109-
top: tooltipY - 8,
110-
transform: "translate(-50%, -100%)",
111-
pointerEvents: "none",
112-
}}
113-
>
114-
<Tooltip text={infiniteLine.label} />
115-
</div>
116-
)}
117115
</div>
118116
)
119117
}

site/components/InteractiveGraphics/InteractiveGraphics.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { Point } from "./Point"
2626
import { Polygon } from "./Polygon"
2727
import { Rect } from "./Rect"
2828
import { Text } from "./Text"
29+
import { Tooltip } from "./Tooltip"
2930
import {
3031
useDoesLineIntersectViewport,
3132
useFilterArrows,
@@ -37,6 +38,7 @@ import {
3738
useFilterTexts,
3839
useIsPointOnScreen,
3940
} from "./hooks"
41+
import { tooltipLayerZIndex } from "./tooltipLayer"
4042

4143
export type GraphicsObjectClickEvent = {
4244
type:
@@ -82,6 +84,11 @@ export const InteractiveGraphics = ({
8284
x: number
8385
y: number
8486
} | null>(null)
87+
const [hoverTooltip, setHoverTooltip] = useState<{
88+
text: string
89+
x: number
90+
y: number
91+
} | null>(null)
8592
const availableLayers: string[] = Array.from(
8693
new Set([
8794
...(graphics.lines?.map((l) => l.layer!).filter(Boolean) ?? []),
@@ -348,6 +355,7 @@ export const InteractiveGraphics = ({
348355
activeStep: showLastStep ? maxStep : activeStep,
349356
realToScreen: realToScreen,
350357
onObjectClicked: handleObjectClicked,
358+
setHoverTooltip,
351359
}
352360

353361
const showToolbar = true
@@ -672,6 +680,20 @@ export const InteractiveGraphics = ({
672680
transform={realToScreen}
673681
/>
674682
))}
683+
{hoverTooltip && (
684+
<div
685+
style={{
686+
position: "absolute",
687+
left: hoverTooltip.x,
688+
top: hoverTooltip.y - 8,
689+
transform: "translate(-50%, -100%)",
690+
pointerEvents: "none",
691+
zIndex: tooltipLayerZIndex,
692+
}}
693+
>
694+
<Tooltip text={hoverTooltip.text} />
695+
</div>
696+
)}
675697
{contextMenu && (
676698
<ContextMenu
677699
x={contextMenu.x}

site/components/InteractiveGraphics/InteractiveState.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,11 @@ export type InteractiveState = {
66
activeStep: number | null
77
realToScreen: Matrix
88
onObjectClicked?: (event: GraphicsObjectClickEvent) => void
9+
setHoverTooltip?: (tooltip: HoverTooltip | null) => void
10+
}
11+
12+
export type HoverTooltip = {
13+
text: string
14+
x: number
15+
y: number
916
}

site/components/InteractiveGraphics/Line.tsx

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import type * as Types from "lib/types"
22
import { applyToPoint } from "transformation-matrix"
33
import type { InteractiveState } from "./InteractiveState"
4-
import { useMemo } from "react"
5-
import { Tooltip } from "./Tooltip"
4+
import { useEffect, useMemo } from "react"
65
import { distToLineSegment } from "site/utils/distToLineSegment"
76
import { defaultColors } from "./defaultColors"
87
import { safeLighten } from "site/utils/safeLighten"
@@ -20,8 +19,13 @@ export const Line = ({
2019
size: { width: number; height: number }
2120
mousePosition: { x: number; y: number } | null
2221
}) => {
23-
const { activeLayers, activeStep, realToScreen, onObjectClicked } =
24-
interactiveState
22+
const {
23+
activeLayers,
24+
activeStep,
25+
realToScreen,
26+
onObjectClicked,
27+
setHoverTooltip,
28+
} = interactiveState
2529
const {
2630
points,
2731
layer,
@@ -63,6 +67,26 @@ export const Line = ({
6367

6468
const baseColor = strokeColor ?? defaultColors[index % defaultColors.length]
6569

70+
useEffect(() => {
71+
if (!isHovered || !line.label || !mousePosition) return
72+
73+
setHoverTooltip?.({
74+
text: line.label,
75+
x: mousePosition.x,
76+
y: mousePosition.y,
77+
})
78+
79+
return () => {
80+
setHoverTooltip?.(null)
81+
}
82+
}, [
83+
isHovered,
84+
line.label,
85+
mousePosition?.x,
86+
mousePosition?.y,
87+
setHoverTooltip,
88+
])
89+
6690
return (
6791
<div
6892
style={{
@@ -118,19 +142,6 @@ export const Line = ({
118142
pointerEvents="none"
119143
/>
120144
</svg>
121-
{isHovered && line.label && mousePosition && (
122-
<div
123-
style={{
124-
position: "absolute",
125-
left: mousePosition.x,
126-
top: mousePosition.y - 8,
127-
transform: "translate(-50%, -100%)",
128-
pointerEvents: "none",
129-
}}
130-
>
131-
<Tooltip text={line.label} />
132-
</div>
133-
)}
134145
</div>
135146
)
136147
}

0 commit comments

Comments
 (0)