Skip to content

Commit 975cbc3

Browse files
committed
feat(plugin): add fixed positioning option for tooltip to prevent clipping in dialogs
1 parent dc99f76 commit 975cbc3

File tree

4 files changed

+366
-10
lines changed

4 files changed

+366
-10
lines changed

playground/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { LegendShowcasePage } from "./pages/LegendShowcase";
1010
import { MultiPlotPage } from "./pages/MultiPlot";
1111
import { PluginsPage } from "./pages/Plugins";
1212
import { Streaming } from "./pages/Streaming";
13+
import { TooltipDialogPage } from "./pages/TooltipDialog";
1314
import { Sidebar } from "./Sidebar";
1415

1516
const RootLayout: Component<ParentProps> = (props) => (
@@ -25,6 +26,7 @@ export const App: Component = () => {
2526
<Route path="/" component={Home} />
2627
<Route path="/examples" component={Examples} />
2728
<Route path="/plugins" component={PluginsPage} />
29+
<Route path="/tooltip-dialog" component={TooltipDialogPage} />
2830
<Route path="/legend-showcase" component={LegendShowcasePage} />
2931
<Route path="/streaming" component={Streaming} />
3032
<Route path="/multi-plot" component={MultiPlotPage} />

playground/Sidebar.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const navItems: NavItem[] = [
1111
{ href: "/", label: "Home", description: "Getting started" },
1212
{ href: "/examples", label: "Examples", description: "Basic chart examples" },
1313
{ href: "/plugins", label: "Plugins", description: "Plugin system showcase" },
14+
{ href: "/tooltip-dialog", label: "Tooltip Dialog", description: "Tooltip in dialog context" },
1415
{ href: "/legend-showcase", label: "Legend Showcase", description: "Legend plugin examples" },
1516
{ href: "/streaming", label: "Streaming", description: "Real-time data updates" },
1617
{ href: "/multi-plot", label: "Multi Plot", description: "Multiple synchronized charts" },
@@ -121,6 +122,16 @@ export const Sidebar: Component = () => {
121122
/>
122123
</svg>
123124
)}
125+
{item.href === "/tooltip-dialog" && (
126+
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
127+
<path
128+
stroke-linecap="round"
129+
stroke-linejoin="round"
130+
stroke-width="2"
131+
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
132+
/>
133+
</svg>
134+
)}
124135
{item.href === "/legend-showcase" && (
125136
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
126137
<path

playground/pages/TooltipDialog.tsx

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
import { type Component, createEffect, createSignal, For, Show } from "solid-js";
2+
3+
import { createPluginBus, SolidUplot } from "../../src";
4+
import { cursor, type CursorPluginMessageBus, tooltip, type TooltipProps } from "../../src/plugins";
5+
6+
const bus1 = createPluginBus<CursorPluginMessageBus>();
7+
const bus2 = createPluginBus<CursorPluginMessageBus>();
8+
9+
const CustomTooltip: Component<TooltipProps> = (props) => {
10+
return (
11+
<div class="rounded-lg border border-gray-200 bg-white p-3 shadow-lg">
12+
<div class="mb-2 border-b border-gray-100 pb-2 text-xs font-semibold text-gray-500">
13+
Data Point {props.cursor.idx}
14+
</div>
15+
<For each={props.seriesData}>
16+
{(series) => {
17+
const value = () => props.u.data[series.seriesIdx]?.[props.cursor.idx];
18+
return (
19+
<div class="flex items-center gap-2 py-1">
20+
<div
21+
class="h-2.5 w-2.5 rounded-full"
22+
style={{ "background-color": series.stroke as string }}
23+
/>
24+
<span class="text-sm font-medium text-gray-700">{series.label}:</span>
25+
<span class="text-sm text-gray-900">{value()?.toFixed(2)}</span>
26+
</div>
27+
);
28+
}}
29+
</For>
30+
</div>
31+
);
32+
};
33+
34+
const chartData = [
35+
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
36+
[10, 20, 15, 25, 30, 28, 35, 40, 38, 45],
37+
[15, 18, 22, 20, 25, 30, 28, 35, 40, 42],
38+
[8, 12, 10, 15, 18, 16, 20, 22, 25, 28],
39+
];
40+
41+
const chartSeries = [
42+
{},
43+
{
44+
label: "Revenue",
45+
stroke: "#3b82f6",
46+
fill: "rgba(59, 130, 246, 0.1)",
47+
width: 2,
48+
},
49+
{
50+
label: "Users",
51+
stroke: "#10b981",
52+
fill: "rgba(16, 185, 129, 0.1)",
53+
width: 2,
54+
},
55+
{
56+
label: "Orders",
57+
stroke: "#f59e0b",
58+
fill: "rgba(245, 158, 11, 0.1)",
59+
width: 2,
60+
},
61+
];
62+
63+
export const TooltipDialogPage: Component = () => {
64+
const [dialogOpen, setDialogOpen] = createSignal(false);
65+
let dialogRef: HTMLDialogElement | undefined;
66+
67+
createEffect(() => {
68+
if (dialogRef) {
69+
if (dialogOpen()) {
70+
dialogRef.showModal();
71+
} else {
72+
dialogRef.close();
73+
}
74+
}
75+
});
76+
77+
return (
78+
<div class="container mx-auto max-w-6xl p-8">
79+
<div class="mb-8 flex items-center justify-between">
80+
<div>
81+
<h1 class="text-3xl font-bold">Tooltip in Dialog</h1>
82+
<p class="mt-2 text-gray-600">Testing tooltip positioning in different contexts</p>
83+
</div>
84+
<a
85+
href="https://github.com/dsnchz/solid-uplot/blob/main/playground/pages/TooltipDialog.tsx"
86+
target="_blank"
87+
rel="noopener noreferrer"
88+
class="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 hover:text-gray-900"
89+
>
90+
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
91+
<path
92+
fill-rule="evenodd"
93+
d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z"
94+
clip-rule="evenodd"
95+
/>
96+
</svg>
97+
View Source on GitHub
98+
</a>
99+
</div>
100+
101+
<div class="space-y-8">
102+
<section class="rounded-lg border border-purple-100 bg-purple-50 p-6">
103+
<h3 class="mb-2 text-lg font-semibold text-purple-900">Testing Instructions</h3>
104+
<ul class="space-y-2 text-sm text-purple-800">
105+
<li class="flex gap-2">
106+
<span class="text-purple-600">1.</span>
107+
<span>
108+
<strong>Chart on Main Page (absolute positioning):</strong> Scroll down and hover
109+
over the chart. The tooltip should correctly position relative to the cursor and
110+
flip at viewport edges.
111+
</span>
112+
</li>
113+
<li class="flex gap-2">
114+
<span class="text-purple-600">2.</span>
115+
<span>
116+
<strong>Chart in Dialog (fixed positioning):</strong> Open the dialog and hover. The
117+
tooltip should not clip at dialog edges and should extend beyond them.
118+
</span>
119+
</li>
120+
<li class="flex gap-2">
121+
<span class="text-purple-600">3.</span>
122+
<span>
123+
<strong>Scroll test:</strong> Scroll the page vertically and test tooltips near the
124+
top/bottom edges. Both should flip correctly based on viewport bounds.
125+
</span>
126+
</li>
127+
</ul>
128+
</section>
129+
130+
<section>
131+
<h2 class="mb-4 text-xl font-semibold text-gray-900">
132+
Chart on Main Page (Absolute Positioning)
133+
</h2>
134+
<p class="mb-4 text-sm text-gray-600">
135+
This chart uses <code class="rounded bg-gray-100 px-1 py-0.5">fixed: false</code>{" "}
136+
(default). The tooltip uses absolute positioning with document coordinates. Scroll the
137+
page and hover near edges to test overflow adjustment.
138+
</p>
139+
<div class="rounded-lg border border-gray-200 bg-white p-4">
140+
<SolidUplot
141+
autoResize
142+
data={chartData}
143+
height={400}
144+
scales={{
145+
x: {
146+
time: false,
147+
},
148+
}}
149+
series={chartSeries}
150+
plugins={[cursor(), tooltip(CustomTooltip, { placement: "top-right" })]}
151+
pluginBus={bus1}
152+
/>
153+
</div>
154+
</section>
155+
156+
<section class="h-96 rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 p-8">
157+
<div class="flex h-full items-center justify-center">
158+
<div class="text-center">
159+
<svg
160+
class="mx-auto h-16 w-16 text-gray-400"
161+
fill="none"
162+
stroke="currentColor"
163+
viewBox="0 0 24 24"
164+
>
165+
<path
166+
stroke-linecap="round"
167+
stroke-linejoin="round"
168+
stroke-width="2"
169+
d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"
170+
/>
171+
</svg>
172+
<h3 class="mt-4 text-lg font-semibold text-gray-700">Spacer for Scrolling Test</h3>
173+
<p class="mt-2 text-sm text-gray-500">
174+
This section adds vertical space to enable scrolling. Scroll up and down to test
175+
tooltip positioning at different scroll positions.
176+
</p>
177+
</div>
178+
</div>
179+
</section>
180+
181+
<section>
182+
<h2 class="mb-4 text-xl font-semibold text-gray-900">
183+
Chart in Dialog Element (Fixed Positioning)
184+
</h2>
185+
<p class="mb-4 text-sm text-gray-600">
186+
This chart uses <code class="rounded bg-gray-100 px-1 py-0.5">fixed: true</code>. The
187+
tooltip uses fixed positioning with viewport coordinates, allowing it to escape the
188+
dialog's clipping boundaries. Click the button to open the dialog and test.
189+
</p>
190+
<button
191+
onClick={() => setDialogOpen(true)}
192+
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
193+
>
194+
Open Dialog with Chart
195+
</button>
196+
197+
<dialog
198+
ref={dialogRef}
199+
onClose={() => setDialogOpen(false)}
200+
class="rounded-lg p-0 shadow-xl backdrop:bg-black backdrop:bg-opacity-50"
201+
style={{
202+
width: "90vw",
203+
"max-width": "800px",
204+
}}
205+
>
206+
<div class="flex flex-col">
207+
<div class="flex items-center justify-between border-b border-gray-200 px-6 py-4">
208+
<h3 class="text-lg font-semibold text-gray-900">Chart in Dialog</h3>
209+
<button
210+
onClick={() => setDialogOpen(false)}
211+
class="rounded-lg p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
212+
aria-label="Close dialog"
213+
>
214+
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
215+
<path
216+
stroke-linecap="round"
217+
stroke-linejoin="round"
218+
stroke-width="2"
219+
d="M6 18L18 6M6 6l12 12"
220+
/>
221+
</svg>
222+
</button>
223+
</div>
224+
<div class="p-6">
225+
<p class="mb-4 text-sm text-gray-600">
226+
Hover over the chart and move the cursor near the edges of the dialog. The tooltip
227+
should extend beyond the dialog boundaries without clipping. It should still flip
228+
based on viewport edges, not dialog edges.
229+
</p>
230+
<Show when={dialogOpen()}>
231+
<div class="rounded-lg border border-gray-200 bg-white p-4">
232+
<SolidUplot
233+
autoResize
234+
data={chartData}
235+
height={400}
236+
scales={{
237+
x: {
238+
time: false,
239+
},
240+
}}
241+
series={chartSeries}
242+
plugins={[
243+
cursor(),
244+
tooltip(CustomTooltip, { placement: "top-right", fixed: true }),
245+
]}
246+
pluginBus={bus2}
247+
/>
248+
</div>
249+
</Show>
250+
</div>
251+
<div class="border-t border-gray-200 bg-gray-50 px-6 py-4">
252+
<button
253+
onClick={() => setDialogOpen(false)}
254+
class="rounded-lg bg-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-300"
255+
>
256+
Close
257+
</button>
258+
</div>
259+
</div>
260+
</dialog>
261+
</section>
262+
263+
<section class="h-96 rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 p-8">
264+
<div class="flex h-full items-center justify-center">
265+
<div class="text-center">
266+
<svg
267+
class="mx-auto h-16 w-16 text-gray-400"
268+
fill="none"
269+
stroke="currentColor"
270+
viewBox="0 0 24 24"
271+
>
272+
<path
273+
stroke-linecap="round"
274+
stroke-linejoin="round"
275+
stroke-width="2"
276+
d="M19 14l-7 7m0 0l-7-7m7 7V3"
277+
/>
278+
</svg>
279+
<h3 class="mt-4 text-lg font-semibold text-gray-700">Additional Scroll Space</h3>
280+
<p class="mt-2 text-sm text-gray-500">
281+
More vertical space to test scrolling behavior. Try opening the dialog while
282+
scrolled down.
283+
</p>
284+
</div>
285+
</div>
286+
</section>
287+
288+
<section class="rounded-lg border border-blue-100 bg-blue-50 p-6">
289+
<h3 class="mb-2 text-lg font-semibold text-blue-900">Expected Behavior</h3>
290+
<ul class="space-y-2 text-sm text-blue-800">
291+
<li class="flex gap-2">
292+
<span class="text-blue-600"></span>
293+
<span>
294+
<strong>Absolute positioning (main page):</strong> Tooltips position relative to
295+
document and flip at viewport edges. Works correctly with page scrolling.
296+
</span>
297+
</li>
298+
<li class="flex gap-2">
299+
<span class="text-blue-600"></span>
300+
<span>
301+
<strong>Fixed positioning (dialog):</strong> Tooltips use viewport coordinates,
302+
escape dialog clipping, and flip at viewport edges.
303+
</span>
304+
</li>
305+
<li class="flex gap-2">
306+
<span class="text-blue-600"></span>
307+
<span>
308+
<strong>Scroll independence:</strong> Both positioning modes work correctly at any
309+
scroll position.
310+
</span>
311+
</li>
312+
<li class="flex gap-2">
313+
<span class="text-blue-600"></span>
314+
<span>
315+
<strong>No clipping:</strong> Fixed tooltips extend beyond dialog boundaries without
316+
being cut off.
317+
</span>
318+
</li>
319+
</ul>
320+
</section>
321+
</div>
322+
</div>
323+
);
324+
};

0 commit comments

Comments
 (0)