Merge pull request #515 from unicorn-utterances/fix-smooth-scrolling-toc

Prevent table of contents smooth scrolling when clicked link is out of view
This commit is contained in:
James Fenn
2023-03-19 19:33:03 -04:00
committed by GitHub

View File

@@ -34,15 +34,54 @@ const { headingsToDisplaySlugs } = Astro.props as {
id: li.firstElementChild.getAttribute("href").slice(1), id: li.firstElementChild.getAttribute("href").slice(1),
})); }));
// return whether an element is currently visible within a scrolling container
function isVisibleInContainer(
container: HTMLElement,
child: HTMLElement
): boolean {
// child element is above the lowest point of the container...
return (
container.scrollTop + container.offsetHeight >
child.offsetTop + child.offsetHeight &&
// child element is below the highest point of the container...
container.scrollTop < child.offsetTop
);
}
// smooth-scroll to a heading when clicked // smooth-scroll to a heading when clicked
function handleAnchorClick(e: Event) { function handleAnchorClick(e: Event) {
e.preventDefault(); e.preventDefault();
const anchor = e.target as HTMLAnchorElement; const anchor = e.target as HTMLAnchorElement;
document const li = anchor.parentElement as HTMLLIElement;
.getElementById(anchor.getAttribute("href").slice(1)) const heading = document.getElementById(
?.scrollIntoView({ anchor.getAttribute("href").slice(1)
behavior: prefersReducedMotion ? "auto" : "smooth", );
}); const isLiVisible = isVisibleInContainer(tocListContainer, li);
/*
This needs to check that both the active and clicked ToC entries
are visible within the scroll container before performing a smooth
scroll to the clicked heading.
Otherwise, since the IntersectionObserver receives continual updates
during a smooth scroll (which there is no way to detect), the ToC will
interrupt the page scroll, since (most) browsers cannot smooth scroll
two containers at once.
*/
const activeLi = tocListContainer.querySelector(
".toc-is-active"
) as HTMLLIElement;
const isActiveVisible =
!activeLi || isVisibleInContainer(tocListContainer, activeLi);
heading?.scrollIntoView({
// only use smooth scrolling if the heading is currently within the TOC scroll area
behavior:
prefersReducedMotion || !(activeLi && isActiveVisible && isLiVisible)
? "auto"
: "smooth",
});
return false; return false;
} }
@@ -70,11 +109,8 @@ const { headingsToDisplaySlugs } = Astro.props as {
// the user hasn't requested reduced motion... // the user hasn't requested reduced motion...
!prefersReducedMotion && !prefersReducedMotion &&
tocListContainer && tocListContainer &&
// the link is below the lowest point of the container... // the link is not currently visible in the container..
(tocListContainer.scrollTop + tocListContainer.offsetHeight < !isVisibleInContainer(tocListContainer, linkRef.li)
linkRef.li.offsetTop + linkRef.li.offsetHeight ||
// the link is above the highest point of the container...
tocListContainer.scrollTop > linkRef.li.offsetTop)
) { ) {
// ...then scroll to center the link in the container // ...then scroll to center the link in the container
tocListContainer.scrollTo({ tocListContainer.scrollTo({