From ede37d94cdc8647606e97b0a769edd4872cd236f Mon Sep 17 00:00:00 2001 From: Alex Krawiec Date: Tue, 10 Feb 2026 13:58:36 -0800 Subject: [PATCH 1/8] Update sidenav to scroll down to current page when necessary --- src/components/focus-active-link.tsx | 82 +++++++++++++++++++++------- 1 file changed, 62 insertions(+), 20 deletions(-) diff --git a/src/components/focus-active-link.tsx b/src/components/focus-active-link.tsx index 94bc037854a3e..f717058a62680 100644 --- a/src/components/focus-active-link.tsx +++ b/src/components/focus-active-link.tsx @@ -7,14 +7,37 @@ 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 firstLink = document.querySelector('[data-sidebar-link]'); + if (!firstLink) { + const noOp = () => {}; + return noOp; + } + + const scrollContainer = findScrollContainer(firstLink); + if (!scrollContainer) { const noOp = () => {}; return noOp; } + const onLinkClick = (e: Event) => { const target = e.target as HTMLElement; if (target.hasAttribute('data-sidebar-link')) { @@ -22,7 +45,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 +55,47 @@ 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 + // Use scrollIntoView with smooth behavior and center the link in the viewport + activeLink.scrollIntoView({ + behavior: 'auto', + block: 'center', + }); + } + }); + + return () => cancelAnimationFrame(timeoutId); }, [activeLinkSelector]); // don't render anything, just exist as a client-side component for the useEffect. return null; From 1e9ff7e44c9f4db6db11a1e49e912d850a8e7c2c Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:59:55 +0000 Subject: [PATCH 2/8] [getsentry/action-github-commit] Auto commit --- src/components/focus-active-link.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/focus-active-link.tsx b/src/components/focus-active-link.tsx index f717058a62680..86747a5517fb2 100644 --- a/src/components/focus-active-link.tsx +++ b/src/components/focus-active-link.tsx @@ -13,7 +13,10 @@ function findScrollContainer(element: Element): Element | null { while (current) { const {overflow, overflowY} = window.getComputedStyle(current); if ( - (overflow === 'auto' || overflow === 'scroll' || overflowY === 'auto' || overflowY === 'scroll') && + (overflow === 'auto' || + overflow === 'scroll' || + overflowY === 'auto' || + overflowY === 'scroll') && current.scrollHeight > current.clientHeight ) { return current; From 6bf1de6daf974fdd344d7aafe9ff152405a21567 Mon Sep 17 00:00:00 2001 From: Alex Krawiec Date: Tue, 10 Feb 2026 15:04:50 -0800 Subject: [PATCH 3/8] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/components/focus-active-link.tsx | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/components/focus-active-link.tsx b/src/components/focus-active-link.tsx index 86747a5517fb2..66de778f17a6e 100644 --- a/src/components/focus-active-link.tsx +++ b/src/components/focus-active-link.tsx @@ -31,14 +31,12 @@ export function ScrollActiveLink({activeLinkSelector}: Props) { useEffect(() => { const firstLink = document.querySelector('[data-sidebar-link]'); if (!firstLink) { - const noOp = () => {}; - return noOp; + return; } const scrollContainer = findScrollContainer(firstLink); if (!scrollContainer) { - const noOp = () => {}; - return noOp; + return; } const onLinkClick = (e: Event) => { @@ -62,6 +60,10 @@ export function ScrollActiveLink({activeLinkSelector}: Props) { return () => { scrollContainer.removeEventListener('click', onLinkClick); scrollContainer.removeEventListener('scroll', onSidebarScroll); + const debouncedHandler = onSidebarScroll as unknown as {cancel?: () => void}; + if (typeof debouncedHandler.cancel === 'function') { + debouncedHandler.cancel(); + } }; }, [activeLinkSelector]); @@ -90,11 +92,16 @@ export function ScrollActiveLink({activeLinkSelector}: Props) { scrollContainer?.scrollTo(scrollX, scrollY); } else { // No stored position (direct navigation, refresh, etc.) - scroll active link into view - // Use scrollIntoView with smooth behavior and center the link in the viewport - activeLink.scrollIntoView({ - behavior: 'auto', - block: 'center', - }); + // Manually adjust only the sidebar scroll container to center the active link + const containerRect = scrollContainer.getBoundingClientRect(); + const linkRect = activeLink.getBoundingClientRect(); + const offsetWithinContainer = linkRect.top - containerRect.top; + const targetScrollTop = + scrollContainer.scrollTop + + offsetWithinContainer - + scrollContainer.clientHeight / 2 + + linkRect.height / 2; + scrollContainer.scrollTo({left: 0, top: targetScrollTop, behavior: 'auto'}); } }); From 64203d5efc86d3d743f1e5c6e0ed1ae2cecc7bc2 Mon Sep 17 00:00:00 2001 From: Alex Krawiec Date: Tue, 10 Feb 2026 15:22:57 -0800 Subject: [PATCH 4/8] Revert --- src/components/focus-active-link.tsx | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/components/focus-active-link.tsx b/src/components/focus-active-link.tsx index 66de778f17a6e..86747a5517fb2 100644 --- a/src/components/focus-active-link.tsx +++ b/src/components/focus-active-link.tsx @@ -31,12 +31,14 @@ export function ScrollActiveLink({activeLinkSelector}: Props) { useEffect(() => { const firstLink = document.querySelector('[data-sidebar-link]'); if (!firstLink) { - return; + const noOp = () => {}; + return noOp; } const scrollContainer = findScrollContainer(firstLink); if (!scrollContainer) { - return; + const noOp = () => {}; + return noOp; } const onLinkClick = (e: Event) => { @@ -60,10 +62,6 @@ export function ScrollActiveLink({activeLinkSelector}: Props) { return () => { scrollContainer.removeEventListener('click', onLinkClick); scrollContainer.removeEventListener('scroll', onSidebarScroll); - const debouncedHandler = onSidebarScroll as unknown as {cancel?: () => void}; - if (typeof debouncedHandler.cancel === 'function') { - debouncedHandler.cancel(); - } }; }, [activeLinkSelector]); @@ -92,16 +90,11 @@ export function ScrollActiveLink({activeLinkSelector}: Props) { scrollContainer?.scrollTo(scrollX, scrollY); } else { // No stored position (direct navigation, refresh, etc.) - scroll active link into view - // Manually adjust only the sidebar scroll container to center the active link - const containerRect = scrollContainer.getBoundingClientRect(); - const linkRect = activeLink.getBoundingClientRect(); - const offsetWithinContainer = linkRect.top - containerRect.top; - const targetScrollTop = - scrollContainer.scrollTop + - offsetWithinContainer - - scrollContainer.clientHeight / 2 + - linkRect.height / 2; - scrollContainer.scrollTo({left: 0, top: targetScrollTop, behavior: 'auto'}); + // Use scrollIntoView with smooth behavior and center the link in the viewport + activeLink.scrollIntoView({ + behavior: 'auto', + block: 'center', + }); } }); From 008c3200b744fd098dc6d4f983bdb14f73d80c96 Mon Sep 17 00:00:00 2001 From: Alex Krawiec Date: Tue, 10 Feb 2026 16:00:46 -0800 Subject: [PATCH 5/8] Remove unneccessary noOp function --- src/components/focus-active-link.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/focus-active-link.tsx b/src/components/focus-active-link.tsx index 86747a5517fb2..b91cbf933cff7 100644 --- a/src/components/focus-active-link.tsx +++ b/src/components/focus-active-link.tsx @@ -31,14 +31,12 @@ export function ScrollActiveLink({activeLinkSelector}: Props) { useEffect(() => { const firstLink = document.querySelector('[data-sidebar-link]'); if (!firstLink) { - const noOp = () => {}; - return noOp; + return undefined; } const scrollContainer = findScrollContainer(firstLink); if (!scrollContainer) { - const noOp = () => {}; - return noOp; + return undefined; } const onLinkClick = (e: Event) => { From 3316b1d96641732dafce3dc8ce01d30b133936a3 Mon Sep 17 00:00:00 2001 From: Alex Krawiec Date: Tue, 10 Feb 2026 16:29:06 -0800 Subject: [PATCH 6/8] Remove unneccessary check --- src/components/focus-active-link.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/focus-active-link.tsx b/src/components/focus-active-link.tsx index b91cbf933cff7..68858c6787fb4 100644 --- a/src/components/focus-active-link.tsx +++ b/src/components/focus-active-link.tsx @@ -85,7 +85,7 @@ export function ScrollActiveLink({activeLinkSelector}: Props) { const scrollX = 0; const scrollY = scrollContainer.scrollTop + currentBoundingRectTop - +previousBoundingRectTop; - scrollContainer?.scrollTo(scrollX, scrollY); + scrollContainer.scrollTo(scrollX, scrollY); } else { // No stored position (direct navigation, refresh, etc.) - scroll active link into view // Use scrollIntoView with smooth behavior and center the link in the viewport From c42c0f4f5f49aa5cf137029511f4a42a02fb5913 Mon Sep 17 00:00:00 2001 From: Alex Krawiec Date: Wed, 11 Feb 2026 13:59:28 -0800 Subject: [PATCH 7/8] Fix scrollIntoView issue --- src/components/focus-active-link.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/focus-active-link.tsx b/src/components/focus-active-link.tsx index 68858c6787fb4..a8512ba844a1d 100644 --- a/src/components/focus-active-link.tsx +++ b/src/components/focus-active-link.tsx @@ -88,10 +88,16 @@ export function ScrollActiveLink({activeLinkSelector}: Props) { scrollContainer.scrollTo(scrollX, scrollY); } else { // No stored position (direct navigation, refresh, etc.) - scroll active link into view - // Use scrollIntoView with smooth behavior and center the link in the viewport - activeLink.scrollIntoView({ + // 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', - block: 'center', }); } }); From 03094ad87f1ee6920144b8e56bd785660fab5f1f Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:00:23 +0000 Subject: [PATCH 8/8] [getsentry/action-github-commit] Auto commit --- src/components/focus-active-link.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/focus-active-link.tsx b/src/components/focus-active-link.tsx index a8512ba844a1d..955be01d8c270 100644 --- a/src/components/focus-active-link.tsx +++ b/src/components/focus-active-link.tsx @@ -94,7 +94,10 @@ export function ScrollActiveLink({activeLinkSelector}: Props) { const containerCenter = containerRect.height / 2; const linkCenter = linkRect.height / 2; const scrollY = - scrollContainer.scrollTop + (linkRect.top - containerRect.top) - containerCenter + linkCenter; + scrollContainer.scrollTop + + (linkRect.top - containerRect.top) - + containerCenter + + linkCenter; scrollContainer.scrollTo({ top: scrollY, behavior: 'auto',