Skip to content

Commit e0d276e

Browse files
committed
replace solid tippy with floating-ui/dom
1 parent aa3157d commit e0d276e

4 files changed

Lines changed: 147 additions & 92 deletions

File tree

pnpm-lock.yaml

Lines changed: 23 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,9 @@
4949
"remark-parse": "^11.0.0",
5050
"remark-rehype": "^11.0.0",
5151
"sass": "^1.72.0",
52+
"@floating-ui/dom": "^1.7.6",
5253
"solid-dismiss": "^1.7.121",
5354
"solid-icons": "^1.1.0",
54-
"solid-tippy": "^0.2.1",
55-
"tippy.js": "^6.3.7",
5655
"undici": "^5.28.2",
5756
"unified": "^11.0.4"
5857
},

site/src/components/Primitives/CodePrimitive.tsx

Lines changed: 0 additions & 15 deletions
This file was deleted.

site/src/routes/package/$name/-components/primitive-name-tooltip-impl.tsx

Lines changed: 123 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1+
import { autoUpdate, computePosition, flip, offset, shift } from "@floating-ui/dom";
2+
import { makeEventListener } from "@solid-primitives/event-listener";
13
import { createResizeObserver } from "@solid-primitives/resize-observer";
2-
import { createMutationObserver } from "@solid-primitives/mutation-observer";
34
import {
45
type Accessor,
56
type Component,
67
createRoot,
78
createSignal,
89
type JSX,
10+
onCleanup,
911
onMount,
1012
} from "solid-js";
11-
import { useTippy } from "solid-tippy";
12-
import { type Content } from "tippy.js";
13+
import { render } from "solid-js/web";
1314
import { BASE } from "~/constants.js";
1415
import type { BundlesizeItem } from "~/types.js";
1516

@@ -55,65 +56,52 @@ const TypeDescriptionContentMap: Record<PrimitiveType, Accessor<JSX.Element>> =
5556
utility: () => "Utility Function",
5657
};
5758

58-
function createTooltipContent(el: HTMLElement, data: BundlesizeItem, type: PrimitiveType): Content {
59+
const TooltipBody: Component<{
60+
data: BundlesizeItem;
61+
type: PrimitiveType;
62+
placement: Accessor<"top" | "bottom">;
63+
}> = props => {
5964
const [target, setTarget] = createSignal<HTMLDivElement>();
6065
const [elSize, setElSize] = createSignal({ height: 0, width: 0 });
61-
const [placement, setPlacement] = createSignal("top");
6266

6367
createResizeObserver(
6468
() => (target() ? [target()!] : []),
6569
() => {
6670
const { height, width } = target()!.getBoundingClientRect();
6771
if (!(height > 0 && width > 0)) return;
68-
6972
setElSize({ height, width });
7073
},
7174
);
7275

73-
createMutationObserver(
74-
() => {
75-
const el = target()?.parentElement?.parentElement;
76-
return el ? [el] : [];
77-
},
78-
{ attributes: true, attributeFilter: ["data-placement"], attributeOldValue: true },
79-
records => {
80-
records.forEach(record => {
81-
if (!(record.target instanceof Element) || !record.attributeName) return;
82-
const dataPlacement = record.target.getAttribute(record.attributeName);
83-
dataPlacement && setPlacement(dataPlacement);
84-
});
85-
},
86-
);
87-
8876
return (
8977
<div
9078
class="relative rounded-[9px] bg-[#2d466d] p-3 text-white dark:bg-[#a6c6df] dark:text-black"
9179
ref={setTarget}
9280
>
9381
<div class="mb-2">
9482
<h2 class="font-semibold opacity-80">Type</h2>
95-
<div class="text-[14px]">{TypeDescriptionContentMap[type]()}</div>
83+
<div class="text-[14px]">{TypeDescriptionContentMap[props.type]()}</div>
9684
</div>
9785
<div>
9886
<h2 class="font-semibold opacity-80">Size</h2>
9987
<div class="w-min">
10088
<div class="flex justify-between gap-2 whitespace-nowrap text-[14px]">
101-
Minified <span>{data.min}</span>
89+
Minified <span>{props.data.min}</span>
10290
</div>
10391
<div class="flex justify-between gap-2 whitespace-nowrap text-[14px]">
104-
GZipped <span>{data.gzip}</span>
92+
GZipped <span>{props.data.gzip}</span>
10593
</div>
10694
</div>
10795
</div>
10896

10997
<TooltipSVG
11098
width={elSize().width}
11199
height={elSize().height}
112-
placement={placement() as "top"}
100+
placement={props.placement()}
113101
/>
114102
</div>
115-
) as Content;
116-
}
103+
);
104+
};
117105

118106
const TooltipSVG: Component<{
119107
width: number;
@@ -140,14 +128,11 @@ const TooltipSVG: Component<{
140128
<defs>
141129
<clipPath id="clipPath3434">
142130
<path
143-
// d="m-5.6602-6.5625v65.502h22.66v-18.697h16v18.697h22.164v-65.502z"
144-
// d="m -4.1602,-6.638771 v 65.502 h 22.66 v -18.697 h 16 v 18.697 h 22.164 v -65.502 z"
145131
d={`M -3,-0 v ${
146132
props.height + 3
147133
} h ${halfedWidthSection()} v -20 h ${widthOfArrow} v 20 h ${halfedWidthSection()} v -${
148134
props.height + 3
149135
} z`}
150-
//
151136
stop-color="#000000"
152137
/>
153138
</clipPath>
@@ -215,6 +200,106 @@ const TooltipSVG: Component<{
215200
);
216201
};
217202

203+
const HOVER_OPEN_DELAY = 100;
204+
const HOVER_CLOSE_DELAY = 150;
205+
206+
/**
207+
* Attach a Floating UI–powered tooltip to `reference`. Lazily creates the
208+
* floating element on first hover, mounts a Solid root into it, disposes
209+
* everything on close. "Interactive" — won't close while the cursor is over
210+
* the floating element, so the user can click links inside the tooltip.
211+
*/
212+
function attachTooltip(
213+
reference: HTMLElement,
214+
data: BundlesizeItem,
215+
type: PrimitiveType,
216+
): () => void {
217+
let floating: HTMLDivElement | undefined;
218+
let dispose: (() => void) | undefined;
219+
let stopAutoUpdate: (() => void) | undefined;
220+
let openTimer: ReturnType<typeof setTimeout> | undefined;
221+
let closeTimer: ReturnType<typeof setTimeout> | undefined;
222+
223+
const [placement, setPlacement] = createSignal<"top" | "bottom">("top");
224+
225+
const update = () => {
226+
if (!floating) return;
227+
computePosition(reference, floating, {
228+
placement: "top",
229+
middleware: [offset(8), flip({ fallbackPlacements: ["bottom"] }), shift({ padding: 8 })],
230+
}).then(({ x, y, placement: p }) => {
231+
if (!floating) return;
232+
Object.assign(floating.style, { left: `${x}px`, top: `${y}px` });
233+
setPlacement(p === "bottom" ? "bottom" : "top");
234+
});
235+
};
236+
237+
const cancelClose = () => {
238+
if (closeTimer) clearTimeout(closeTimer);
239+
closeTimer = undefined;
240+
};
241+
242+
const close = () => {
243+
stopAutoUpdate?.();
244+
stopAutoUpdate = undefined;
245+
dispose?.();
246+
dispose = undefined;
247+
floating?.remove();
248+
floating = undefined;
249+
};
250+
251+
const scheduleClose = () => {
252+
cancelClose();
253+
closeTimer = setTimeout(close, HOVER_CLOSE_DELAY);
254+
};
255+
256+
const open = () => {
257+
if (floating) return;
258+
floating = document.createElement("div");
259+
floating.style.cssText = "position:absolute;top:0;left:0;z-index:9999;";
260+
document.body.appendChild(floating);
261+
262+
dispose = render(
263+
() => <TooltipBody data={data} type={type} placement={placement} />,
264+
floating,
265+
);
266+
stopAutoUpdate = autoUpdate(reference, floating, update);
267+
268+
// Track hover over the floating element too so "interactive" mode works —
269+
// moving the cursor from the reference into the tooltip must not close it.
270+
floating.addEventListener("mouseenter", cancelClose);
271+
floating.addEventListener("mouseleave", scheduleClose);
272+
};
273+
274+
const cancelOpen = () => {
275+
if (openTimer) clearTimeout(openTimer);
276+
openTimer = undefined;
277+
};
278+
279+
const scheduleOpen = () => {
280+
cancelClose();
281+
if (floating || openTimer) return;
282+
openTimer = setTimeout(() => {
283+
openTimer = undefined;
284+
open();
285+
}, HOVER_OPEN_DELAY);
286+
};
287+
288+
const stopEnter = makeEventListener(reference, "mouseenter", scheduleOpen);
289+
const stopLeave = makeEventListener(reference, "mouseleave", () => {
290+
cancelOpen();
291+
if (floating) scheduleClose();
292+
});
293+
294+
return () => {
295+
stopEnter();
296+
stopLeave();
297+
cancelOpen();
298+
cancelClose();
299+
close();
300+
};
301+
}
302+
218303
export function createPrimitiveNameTooltips(props: {
219304
target: HTMLElement;
220305
primitives: BundlesizeItem[];
@@ -232,21 +317,14 @@ export function createPrimitiveNameTooltips(props: {
232317

233318
const type = getTypeOfPrimitive(data.name);
234319

235-
let dispose: () => void;
236-
237-
useTippy(() => el, {
238-
props: {
239-
onMount(instance) {
240-
createRoot(_dispose => {
241-
dispose = _dispose;
242-
instance.setContent(createTooltipContent(el, data, type));
243-
});
244-
},
245-
onHidden: () => dispose(),
246-
interactive: true,
247-
appendTo: () => document.body,
248-
},
249-
hidden: true,
320+
// Each tooltip lives in its own root so it can be torn down independently
321+
// when the surrounding readme element is removed (e.g. on route change).
322+
createRoot(disposeRoot => {
323+
const detach = attachTooltip(el, data, type);
324+
onCleanup(() => {
325+
detach();
326+
disposeRoot();
327+
});
250328
});
251329
}
252330
});

0 commit comments

Comments
 (0)