diff --git a/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx b/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx index 395660120e..c0b10ae51c 100644 --- a/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx @@ -49,7 +49,8 @@ const createAnchorHeading = ( aria-hidden="true" tabIndex={-1} className={clsx('anchor', `anchor__${Tag}`, { - [styles.enhancedAnchor]: !hideOnScroll, + [styles.anchorWithHideOnScrollNavbar]: hideOnScroll, + [styles.anchorWithStickyNavbar]: !hideOnScroll, })} id={id} /> diff --git a/packages/docusaurus-theme-classic/src/theme/Heading/styles.module.css b/packages/docusaurus-theme-classic/src/theme/Heading/styles.module.css index 4e24477449..21900cd80c 100644 --- a/packages/docusaurus-theme-classic/src/theme/Heading/styles.module.css +++ b/packages/docusaurus-theme-classic/src/theme/Heading/styles.module.css @@ -5,6 +5,16 @@ * LICENSE file in the root directory of this source tree. */ -.enhancedAnchor { - top: calc(var(--ifm-navbar-height) * -1 - 0.5rem); +/* +When the navbar is sticky, ensure that on anchor click, +the browser does not scroll that anchor behind the navbar +See https://twitter.com/JoshWComeau/status/1332015868725891076 + */ +.anchorWithStickyNavbar { + scroll-margin-top: calc(var(--ifm-navbar-height) + 0.5rem); + /* top: calc(var(--ifm-navbar-height) * -1 - 0.5rem); */ +} + +.anchorWithHideOnScrollNavbar { + scroll-margin-top: 0.5rem; } diff --git a/packages/docusaurus-theme-classic/src/theme/hooks/useTOCHighlight.ts b/packages/docusaurus-theme-classic/src/theme/hooks/useTOCHighlight.ts index e4c978cdcb..6d46cdea2b 100644 --- a/packages/docusaurus-theme-classic/src/theme/hooks/useTOCHighlight.ts +++ b/packages/docusaurus-theme-classic/src/theme/hooks/useTOCHighlight.ts @@ -7,6 +7,7 @@ import {Params} from '@theme/hooks/useTOCHighlight'; import {useEffect, useRef} from 'react'; +import {useThemeConfig} from '@docusaurus/theme-common'; // If the anchor has no height and is just a "marker" in the dom; we'll use the parent (normally the link text) rect boundaries instead function getVisibleBoundingClientRect(element: HTMLElement): DOMRect { @@ -30,11 +31,13 @@ function getAnchors() { return Array.from(document.querySelectorAll(selector)) as HTMLElement[]; } -function getActiveAnchor(): Element | null { +function getActiveAnchor({ + anchorTopOffset, +}: { + anchorTopOffset: number; +}): Element | null { const anchors = getAnchors(); - const anchorTopOffset = 100; // Skip anchors that are too close to the viewport top - // Naming is hard // The "nextVisibleAnchor" is the first anchor that appear under the viewport top boundary // Note: it does not mean this anchor is visible yet, but if user continues scrolling down, it will be the first to become visible @@ -74,9 +77,30 @@ function getLinks(linkClassName: string) { ) as HTMLAnchorElement[]; } +function getNavbarHeight(): number { + // Not ideal to obtain actual height this way + // Using TS ! (not ?) because otherwise a bad selector would be un-noticed + return document.querySelector('.navbar')!.clientHeight; +} + +function useAnchorTopOffsetRef() { + const anchorTopOffsetRef = useRef(0); + const { + navbar: {hideOnScroll}, + } = useThemeConfig(); + + useEffect(() => { + anchorTopOffsetRef.current = hideOnScroll ? 0 : getNavbarHeight(); + }, [hideOnScroll]); + + return anchorTopOffsetRef; +} + function useTOCHighlight(params: Params): void { const lastActiveLinkRef = useRef(undefined); + const anchorTopOffsetRef = useAnchorTopOffsetRef(); + useEffect(() => { const {linkClassName, linkActiveClassName} = params; @@ -94,7 +118,9 @@ function useTOCHighlight(params: Params): void { function updateActiveLink() { const links = getLinks(linkClassName); - const activeAnchor = getActiveAnchor(); + const activeAnchor = getActiveAnchor({ + anchorTopOffset: anchorTopOffsetRef.current, + }); const activeLink = links.find( (link) => activeAnchor && activeAnchor.id === getLinkAnchorValue(link), ); @@ -113,7 +139,7 @@ function useTOCHighlight(params: Params): void { document.removeEventListener('scroll', updateActiveLink); document.removeEventListener('resize', updateActiveLink); }; - }, [params]); + }, [params, anchorTopOffsetRef]); } export default useTOCHighlight;