diff --git a/src/components/focus-active-link.tsx b/src/components/focus-active-link.tsx index 94bc037854a3e..955be01d8c270 100644 --- a/src/components/focus-active-link.tsx +++ b/src/components/focus-active-link.tsx @@ -7,14 +7,38 @@ type Props = { activeLinkSelector: string; }; +// Helper to find the actual scrollable container +function findScrollContainer(element: Element): Element | null { + let current: Element | null = element; + while (current) { + const {overflow, overflowY} = window.getComputedStyle(current); + if ( + (overflow === 'auto' || + overflow === 'scroll' || + overflowY === 'auto' || + overflowY === 'scroll') && + current.scrollHeight > current.clientHeight + ) { + return current; + } + current = current.parentElement; + } + return null; +} + /** Make sure the active link is visible in the sidebar */ export function ScrollActiveLink({activeLinkSelector}: Props) { useEffect(() => { - const sidebar = document.querySelector('[data-sidebar-link]')?.closest('aside'); - if (!sidebar) { - const noOp = () => {}; - return noOp; + const firstLink = document.querySelector('[data-sidebar-link]'); + if (!firstLink) { + return undefined; } + + const scrollContainer = findScrollContainer(firstLink); + if (!scrollContainer) { + return undefined; + } + const onLinkClick = (e: Event) => { const target = e.target as HTMLElement; if (target.hasAttribute('data-sidebar-link')) { @@ -22,7 +46,7 @@ export function ScrollActiveLink({activeLinkSelector}: Props) { sessionStorage.setItem('sidebar-link-position', top.toString()); } }; - sidebar.addEventListener('click', onLinkClick); + scrollContainer.addEventListener('click', onLinkClick); // track active link position on scroll as well const onSidebarScroll = debounce(() => { const activeLink = document.querySelector(activeLinkSelector); @@ -32,28 +56,56 @@ export function ScrollActiveLink({activeLinkSelector}: Props) { } }, 50); - sidebar.addEventListener('scroll', onSidebarScroll); + scrollContainer.addEventListener('scroll', onSidebarScroll); return () => { - sidebar.removeEventListener('click', onLinkClick); - sidebar.removeEventListener('scroll', onSidebarScroll); + scrollContainer.removeEventListener('click', onLinkClick); + scrollContainer.removeEventListener('scroll', onSidebarScroll); }; }, [activeLinkSelector]); useEffect(() => { - const activeLink = document.querySelector(activeLinkSelector); - const sidebar = activeLink?.closest('aside')!; - if (!activeLink || !sidebar) { - return; - } - const previousBoundingRectTop = sessionStorage.getItem('sidebar-link-position'); - const currentBoundingRectTop = activeLink.getBoundingClientRect().top; - // scroll the sidebar to make sure the active link is visible & has the same position as when it was clicked - if (!previousBoundingRectTop) { - return; - } - const scrollX = 0; - const scrollY = sidebar.scrollTop + currentBoundingRectTop - +previousBoundingRectTop; - sidebar?.scrollTo(scrollX, scrollY); + // Use requestAnimationFrame to ensure DOM is fully rendered + const timeoutId = requestAnimationFrame(() => { + const activeLink = document.querySelector(activeLinkSelector); + if (!activeLink) { + return; + } + + // Find the actual scrollable container (could be .toc, .sidebar, or another element) + const scrollContainer = findScrollContainer(activeLink); + if (!scrollContainer) { + return; + } + + const previousBoundingRectTop = sessionStorage.getItem('sidebar-link-position'); + const currentBoundingRectTop = activeLink.getBoundingClientRect().top; + + // If we have a stored position, restore it to maintain the same visual position + if (previousBoundingRectTop) { + const scrollX = 0; + const scrollY = + scrollContainer.scrollTop + currentBoundingRectTop - +previousBoundingRectTop; + scrollContainer.scrollTo(scrollX, scrollY); + } else { + // No stored position (direct navigation, refresh, etc.) - scroll active link into view + // Calculate the scroll position to center the active link in the scroll container + const containerRect = scrollContainer.getBoundingClientRect(); + const linkRect = activeLink.getBoundingClientRect(); + const containerCenter = containerRect.height / 2; + const linkCenter = linkRect.height / 2; + const scrollY = + scrollContainer.scrollTop + + (linkRect.top - containerRect.top) - + containerCenter + + linkCenter; + scrollContainer.scrollTo({ + top: scrollY, + behavior: 'auto', + }); + } + }); + + return () => cancelAnimationFrame(timeoutId); }, [activeLinkSelector]); // don't render anything, just exist as a client-side component for the useEffect. return null;