1+ import { autoUpdate , computePosition , flip , offset , shift } from "@floating-ui/dom" ;
2+ import { makeEventListener } from "@solid-primitives/event-listener" ;
13import { createResizeObserver } from "@solid-primitives/resize-observer" ;
2- import { createMutationObserver } from "@solid-primitives/mutation-observer" ;
34import {
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" ;
1314import { BASE } from "~/constants.js" ;
1415import 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
118106const 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+
218303export 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