From b4d6d7859e6e9f49d8a3e4096981bdb8f115562d Mon Sep 17 00:00:00 2001 From: mpcgird Date: Thu, 11 Dec 2025 22:53:49 +0000 Subject: [PATCH 1/2] Fix for mouse hover on a timline not being updates after resize Refactor MousePositionProvider to improve reactivity during resize, scroll, and layout shifts. --- .../app/components/primitives/Timeline.tsx | 81 +++++++++++++++---- 1 file changed, 65 insertions(+), 16 deletions(-) diff --git a/apps/webapp/app/components/primitives/Timeline.tsx b/apps/webapp/app/components/primitives/Timeline.tsx index dfb3253750..9b685fd1fa 100644 --- a/apps/webapp/app/components/primitives/Timeline.tsx +++ b/apps/webapp/app/components/primitives/Timeline.tsx @@ -1,11 +1,11 @@ import { - Component, ComponentPropsWithoutRef, Fragment, ReactNode, createContext, useCallback, useContext, + useEffect, useRef, useState, } from "react"; @@ -19,28 +19,77 @@ const MousePositionContext = createContext(undefined) export function MousePositionProvider({ children }: { children: ReactNode }) { const ref = useRef(null); const [position, setPosition] = useState(undefined); + const lastClient = useRef<{ clientX: number; clientY: number } | null>(null); + const rafId = useRef(null); - const handleMouseMove = useCallback( - (e: React.MouseEvent) => { - if (!ref.current) { - setPosition(undefined); - return; - } + const computeFromClient = useCallback((clientX: number, clientY: number) => { + if (!ref.current) { + setPosition(undefined); + return; + } - const { top, left, width, height } = ref.current.getBoundingClientRect(); - const x = (e.clientX - left) / width; - const y = (e.clientY - top) / height; + const { top, left, width, height } = ref.current.getBoundingClientRect(); + const x = (clientX - left) / width; + const y = (clientY - top) / height; - if (x < 0 || x > 1 || y < 0 || y > 1) { - setPosition(undefined); - return; - } + if (x < 0 || x > 1 || y < 0 || y > 1) { + setPosition(undefined); + return; + } - setPosition({ x, y }); + setPosition({ x, y }); + }, []); + + const handleMouseMove = useCallback( + (e: React.MouseEvent) => { + lastClient.current = { clientX: e.clientX, clientY: e.clientY }; + computeFromClient(e.clientX, e.clientY); }, - [ref.current] + [computeFromClient] ); + // Recalculate the relative position when the container resizes or the window/ancestors scroll. + useEffect(() => { + if (!ref.current) return; + + const ro = new ResizeObserver(() => { + const lc = lastClient.current; + if (lc) computeFromClient(lc.clientX, lc.clientY); + }); + ro.observe(ref.current); + + const onRecalc = () => { + const lc = lastClient.current; + if (lc) computeFromClient(lc.clientX, lc.clientY); + }; + + window.addEventListener("resize", onRecalc); + // Use capture to catch scroll on any ancestor that impacts bounding rect + window.addEventListener("scroll", onRecalc, true); + + return () => { + ro.disconnect(); + window.removeEventListener("resize", onRecalc); + window.removeEventListener("scroll", onRecalc, true); + }; + }, [computeFromClient]); + + useEffect(() => { + if (position === undefined || !lastClient.current) return; + + const tick = () => { + const lc = lastClient.current; + if (lc) computeFromClient(lc.clientX, lc.clientY); + rafId.current = requestAnimationFrame(tick); + }; + + rafId.current = requestAnimationFrame(tick); + return () => { + if (rafId.current !== null) cancelAnimationFrame(rafId.current); + rafId.current = null; + }; + }, [position, computeFromClient]); + return (
Date: Mon, 15 Dec 2025 23:28:27 +0200 Subject: [PATCH 2/2] optimized the animation frame loop --- .../app/components/primitives/Timeline.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/primitives/Timeline.tsx b/apps/webapp/app/components/primitives/Timeline.tsx index 9b685fd1fa..16a2cd6edb 100644 --- a/apps/webapp/app/components/primitives/Timeline.tsx +++ b/apps/webapp/app/components/primitives/Timeline.tsx @@ -75,12 +75,24 @@ export function MousePositionProvider({ children }: { children: ReactNode }) { }, [computeFromClient]); useEffect(() => { - if (position === undefined || !lastClient.current) return; + if (position === undefined || !lastClient.current || !ref.current) return; + + const isAnimating = () => { + if (!ref.current) return false; + const styles = window.getComputedStyle(ref.current); + return styles.transition !== "none" || styles.animation !== "none"; + }; const tick = () => { const lc = lastClient.current; - if (lc) computeFromClient(lc.clientX, lc.clientY); - rafId.current = requestAnimationFrame(tick); + if (lc) { + computeFromClient(lc.clientX, lc.clientY); + if (isAnimating()) { + rafId.current = requestAnimationFrame(tick); + } else { + rafId.current = null; + } + } }; rafId.current = requestAnimationFrame(tick);