@@ -8,31 +8,20 @@ import { cn } from '../../lib/cn'
88const TOOLTIP_OFFSET = 16
99const EDGE_GUTTER = 16
1010const EDGE_THRESHOLD = 360
11- const MIN_FRAME_MS = 16
1211
1312/**
14- * Resolved position and motion of a floating tooltip. `x`/`y` are viewport
13+ * Resolved position of a floating tooltip. `x`/`y` are whole-pixel viewport
1514 * coordinates the tooltip anchors to; `alignX`/`alignY` flip the tooltip away
16- * from the nearest viewport edge; `skew`/`scale*` add the velocity-reactive
17- * flourish while the pointer is moving.
15+ * from the nearest viewport edge.
1816 */
1917export interface FloatingTooltipState {
2018 visible : boolean
2119 x : number
2220 y : number
23- skew : number
24- scaleX : number
25- scaleY : number
2621 alignX : 'left' | 'right'
2722 alignY : 'above' | 'below'
2823}
2924
30- interface PointerSnapshot {
31- x : number
32- y : number
33- time : number
34- }
35-
3625/**
3726 * Pointer/focus event handlers that drive a {@link useFloatingTooltip}. Spread
3827 * onto the element that should reveal the tooltip on hover or focus.
@@ -50,9 +39,6 @@ const HIDDEN_STATE: FloatingTooltipState = {
5039 visible : false ,
5140 x : 0 ,
5241 y : 0 ,
53- skew : 0 ,
54- scaleX : 1 ,
55- scaleY : 1 ,
5642 alignX : 'left' ,
5743 alignY : 'below' ,
5844}
@@ -71,48 +57,32 @@ export function useFloatingTooltip(canShow: (target: HTMLElement) => boolean): {
7157 const canShowRef = React . useRef ( canShow )
7258 canShowRef . current = canShow
7359
74- const lastPointerRef = React . useRef < PointerSnapshot | null > ( null )
7560 const [ state , setState ] = React . useState < FloatingTooltipState > ( HIDDEN_STATE )
7661
7762 const handlers = React . useMemo < FloatingTooltipHandlers > ( ( ) => {
78- const hide = ( ) => {
79- lastPointerRef . current = null
80- setState ( ( current ) => ( current . visible ? HIDDEN_STATE : current ) )
81- }
82-
83- const showStatic = ( clientX : number , clientY : number ) => {
84- lastPointerRef . current = { x : clientX , y : clientY , time : performance . now ( ) }
85- setState ( {
86- visible : true ,
87- ...getTooltipPosition ( clientX , clientY ) ,
88- skew : 0 ,
89- scaleX : 1 ,
90- scaleY : 1 ,
91- } )
63+ const hide = ( ) => setState ( ( current ) => ( current . visible ? HIDDEN_STATE : current ) )
64+
65+ const show = ( clientX : number , clientY : number ) => {
66+ const next = getTooltipPosition ( clientX , clientY )
67+ setState ( ( current ) =>
68+ current . visible &&
69+ current . x === next . x &&
70+ current . y === next . y &&
71+ current . alignX === next . alignX &&
72+ current . alignY === next . alignY
73+ ? current
74+ : { visible : true , ...next }
75+ )
9276 }
9377
9478 return {
9579 onPointerEnter : ( event ) => {
9680 if ( ! canShowRef . current ( event . currentTarget ) ) return
97- showStatic ( event . clientX , event . clientY )
81+ show ( event . clientX , event . clientY )
9882 } ,
9983 onPointerMove : ( event ) => {
10084 if ( ! canShowRef . current ( event . currentTarget ) ) return
101- const now = performance . now ( )
102- const previous = lastPointerRef . current
103- const elapsed = previous ? Math . max ( now - previous . time , MIN_FRAME_MS ) : MIN_FRAME_MS
104- const velocityX = previous ? ( ( event . clientX - previous . x ) / elapsed ) * MIN_FRAME_MS : 0
105- const velocityY = previous ? ( ( event . clientY - previous . y ) / elapsed ) * MIN_FRAME_MS : 0
106- const velocity = Math . hypot ( velocityX , velocityY )
107-
108- lastPointerRef . current = { x : event . clientX , y : event . clientY , time : now }
109- setState ( {
110- visible : true ,
111- ...getTooltipPosition ( event . clientX , event . clientY ) ,
112- skew : clamp ( velocityX * 0.11 , - 6 , 6 ) ,
113- scaleX : 1 + Math . min ( 0.035 , velocity / 1100 ) ,
114- scaleY : 1 - Math . min ( 0.02 , velocity / 1500 ) ,
115- } )
85+ show ( event . clientX , event . clientY )
11686 } ,
11787 onPointerLeave : hide ,
11888 onPointerDown : hide ,
@@ -121,14 +91,7 @@ export function useFloatingTooltip(canShow: (target: HTMLElement) => boolean): {
12191 if ( ! canShowRef . current ( target ) ) return
12292 if ( ! isFocusVisible ( target ) ) return
12393 const rect = target . getBoundingClientRect ( )
124- lastPointerRef . current = null
125- setState ( {
126- visible : true ,
127- ...getTooltipPosition ( rect . left + rect . width / 2 , rect . bottom ) ,
128- skew : 0 ,
129- scaleX : 1 ,
130- scaleY : 1 ,
131- } )
94+ show ( rect . left + rect . width / 2 , rect . bottom )
13295 } ,
13396 onBlur : hide ,
13497 }
@@ -248,14 +211,11 @@ export const FloatingTooltip = React.memo(function FloatingTooltip({
248211 aria-hidden = { role ? undefined : 'true' }
249212 data-native-surface-overlay = ''
250213 className = { cn (
251- 'pointer-events-none fixed top-0 left-0 z-[var(--z-tooltip)] w-fit max-w-[min(16rem,calc(100vw-2rem))] rounded-lg border border-[var(--border)] bg-[var(--bg)] px-2 py-1.5 text-[var(--text-body)] text-caption opacity-100 shadow-sm transition-[opacity,filter, transform] duration-150 ease-out' ,
214+ 'pointer-events-none fixed top-0 left-0 z-[var(--z-tooltip)] w-fit max-w-[min(16rem,calc(100vw-2rem))] rounded-lg border border-[var(--border)] bg-[var(--bg)] px-2 py-1.5 text-[var(--text-body)] text-caption opacity-100 shadow-sm transition-[opacity,transform] duration-150 ease-out' ,
252215 'motion-reduce:transition-none' ,
253216 className
254217 ) }
255- style = { {
256- transform : `${ getTooltipTranslate ( state , offset ) } skew(${ state . skew } deg) scale(${ state . scaleX } , ${ state . scaleY } )` ,
257- transformOrigin : state . alignX === 'left' ? '12px 12px' : 'calc(100% - 12px) 12px' ,
258- } }
218+ style = { { transform : getTooltipTranslate ( state , offset ) } }
259219 >
260220 { children ?? < span className = 'block whitespace-normal break-words text-left' > { label } </ span > }
261221 </ div > ,
@@ -268,15 +228,15 @@ function getTooltipPosition(
268228 clientY : number
269229) : Pick < FloatingTooltipState , 'x' | 'y' | 'alignX' | 'alignY' > {
270230 if ( typeof window === 'undefined' ) {
271- return { x : clientX , y : clientY , alignX : 'left' , alignY : 'below' }
231+ return { x : Math . round ( clientX ) , y : Math . round ( clientY ) , alignX : 'left' , alignY : 'below' }
272232 }
273233
274234 const alignX = window . innerWidth - clientX < EDGE_THRESHOLD ? 'right' : 'left'
275235 const alignY = window . innerHeight - clientY < EDGE_THRESHOLD / 2 ? 'above' : 'below'
276236
277237 return {
278- x : clamp ( clientX , EDGE_GUTTER , window . innerWidth - EDGE_GUTTER ) ,
279- y : clamp ( clientY , EDGE_GUTTER , window . innerHeight - EDGE_GUTTER ) ,
238+ x : Math . round ( clamp ( clientX , EDGE_GUTTER , window . innerWidth - EDGE_GUTTER ) ) ,
239+ y : Math . round ( clamp ( clientY , EDGE_GUTTER , window . innerHeight - EDGE_GUTTER ) ) ,
280240 alignX,
281241 alignY,
282242 }
0 commit comments