From ac630f827929b45bb50ef8eed17ccf8a41728caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Thu, 9 Oct 2025 12:34:54 +0200 Subject: [PATCH] fix(theme): Fix CSS `scroll-margin-top` when clicking footnote items, factorize code (#11466) --- .../src/theme/Heading/index.tsx | 18 +-- .../src/theme/Heading/styles.module.css | 13 -- .../src/theme/MDXComponents/A/index.tsx | 35 +---- .../theme/MDXComponents/A/styles.module.css | 20 --- .../src/theme/MDXComponents/Li.tsx | 7 +- packages/docusaurus-theme-common/src/index.ts | 2 + .../src/utils/anchorUtils.module.css | 14 ++ .../src/utils/anchorUtils.ts | 31 ++++ .../_docs tests/tests/footnotes.mdx | 146 ++++++++++++++++++ 9 files changed, 211 insertions(+), 75 deletions(-) delete mode 100644 packages/docusaurus-theme-classic/src/theme/MDXComponents/A/styles.module.css create mode 100644 packages/docusaurus-theme-common/src/utils/anchorUtils.module.css create mode 100644 packages/docusaurus-theme-common/src/utils/anchorUtils.ts create mode 100644 website/_dogfooding/_docs tests/tests/footnotes.mdx diff --git a/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx b/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx index 8f7c4fc2bf..415cd65f2e 100644 --- a/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx @@ -8,18 +8,16 @@ import React, {type ReactNode} from 'react'; import clsx from 'clsx'; import {translate} from '@docusaurus/Translate'; -import {useThemeConfig} from '@docusaurus/theme-common'; +import {useAnchorTargetClassName} from '@docusaurus/theme-common'; import Link from '@docusaurus/Link'; import useBrokenLinks from '@docusaurus/useBrokenLinks'; import type {Props} from '@theme/Heading'; - -import styles from './styles.module.css'; +import './styles.module.css'; export default function Heading({as: As, id, ...props}: Props): ReactNode { const brokenLinks = useBrokenLinks(); - const { - navbar: {hideOnScroll}, - } = useThemeConfig(); + const anchorTargetClassName = useAnchorTargetClassName(id); + // H1 headings do not need an id because they don't appear in the TOC. if (As === 'h1' || !id) { return ; @@ -41,13 +39,7 @@ export default function Heading({as: As, id, ...props}: Props): ReactNode { return ( {props.children} - ); -} export default function MDXA(props: Props): ReactNode { - if (isFootnoteRef(props)) { - return ; - } - return ; + // MDX Footnotes have ids such as + const anchorTargetClassName = useAnchorTargetClassName(props.id); + + return ( + + ); } diff --git a/packages/docusaurus-theme-classic/src/theme/MDXComponents/A/styles.module.css b/packages/docusaurus-theme-classic/src/theme/MDXComponents/A/styles.module.css deleted file mode 100644 index 295757363a..0000000000 --- a/packages/docusaurus-theme-classic/src/theme/MDXComponents/A/styles.module.css +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -/* -When the navbar is sticky, ensure that on footnote click, -the browser does not scroll to the ref behind the navbar -See https://github.com/facebook/docusaurus/issues/11232 -See also headings case https://x.com/JoshWComeau/status/1332015868725891076 - */ -.footnoteRefStickyNavbar { - scroll-margin-top: calc(var(--ifm-navbar-height) + 0.5rem); -} - -.footnoteRefHideOnScrollNavbar { - scroll-margin-top: 0.5rem; -} diff --git a/packages/docusaurus-theme-classic/src/theme/MDXComponents/Li.tsx b/packages/docusaurus-theme-classic/src/theme/MDXComponents/Li.tsx index 74a8a4add4..bb2a3d6497 100644 --- a/packages/docusaurus-theme-classic/src/theme/MDXComponents/Li.tsx +++ b/packages/docusaurus-theme-classic/src/theme/MDXComponents/Li.tsx @@ -6,12 +6,17 @@ */ import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; import useBrokenLinks from '@docusaurus/useBrokenLinks'; +import {useAnchorTargetClassName} from '@docusaurus/theme-common'; import type {Props} from '@theme/MDXComponents/Li'; export default function MDXLi(props: Props): ReactNode | undefined { // MDX Footnotes have ids such as
  • useBrokenLinks().collectAnchor(props.id); + const anchorTargetClassName = useAnchorTargetClassName(props.id); - return
  • ; + return ( +
  • + ); } diff --git a/packages/docusaurus-theme-common/src/index.ts b/packages/docusaurus-theme-common/src/index.ts index 7243780701..1c9e15f1dc 100644 --- a/packages/docusaurus-theme-common/src/index.ts +++ b/packages/docusaurus-theme-common/src/index.ts @@ -109,6 +109,8 @@ export { useSearchLinkCreator, } from './hooks/useSearchPage'; +export {useAnchorTargetClassName} from './utils/anchorUtils'; + export {isMultiColumnFooterLinks} from './utils/footerUtils'; export {isRegexpStringMatch} from './utils/regexpUtils'; diff --git a/packages/docusaurus-theme-common/src/utils/anchorUtils.module.css b/packages/docusaurus-theme-common/src/utils/anchorUtils.module.css new file mode 100644 index 0000000000..1904425c03 --- /dev/null +++ b/packages/docusaurus-theme-common/src/utils/anchorUtils.module.css @@ -0,0 +1,14 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +.anchorTargetStickyNavbar { + scroll-margin-top: calc(var(--ifm-navbar-height) + 0.5rem); +} + +.anchorTargetHideOnScrollNavbar { + scroll-margin-top: 0.5rem; +} diff --git a/packages/docusaurus-theme-common/src/utils/anchorUtils.ts b/packages/docusaurus-theme-common/src/utils/anchorUtils.ts new file mode 100644 index 0000000000..64c2832644 --- /dev/null +++ b/packages/docusaurus-theme-common/src/utils/anchorUtils.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {useThemeConfig} from './useThemeConfig'; +import styles from './anchorUtils.module.css'; + +/** + * When the navbar is sticky, this ensures that when clicking a hash link, + * we do not navigate to an anchor that will appear below the navbar. + * This happens in particular for MDX headings and footnotes. + * + * See https://github.com/facebook/docusaurus/issues/11232 + * See also headings case https://x.com/JoshWComeau/status/1332015868725891076 + */ +export function useAnchorTargetClassName( + id: string | undefined, +): string | undefined { + const { + navbar: {hideOnScroll}, + } = useThemeConfig(); + if (typeof id === 'undefined') { + return undefined; + } + return hideOnScroll + ? styles.anchorTargetHideOnScrollNavbar + : styles.anchorTargetStickyNavbar; +} diff --git a/website/_dogfooding/_docs tests/tests/footnotes.mdx b/website/_dogfooding/_docs tests/tests/footnotes.mdx new file mode 100644 index 0000000000..da28ea2d66 --- /dev/null +++ b/website/_dogfooding/_docs tests/tests/footnotes.mdx @@ -0,0 +1,146 @@ +# Footnotes + +Lorem ipsum dolor sit amet [^1] [^2] + +Lorem ipsum dolor sit amet [^3] + +Lorem ipsum dolor sit amet [^4] + +Lorem ipsum dolor sit amet [^5] + +Lorem ipsum dolor sit amet [^6] + +Lorem ipsum dolor sit amet [^7] + +Lorem ipsum dolor sit amet [^8] + +Lorem ipsum dolor sit amet [^9] + +Lorem ipsum dolor sit amet [^10] + +Lorem ipsum dolor sit amet [^11] + +Lorem ipsum dolor sit amet [^12] + +Lorem ipsum dolor sit amet [^13] + +Lorem ipsum dolor sit amet [^14] + +Lorem ipsum dolor sit amet [^15] + +Lorem ipsum dolor sit amet [^16] + +Lorem ipsum dolor sit amet [^17] + +Lorem ipsum dolor sit amet [^18] + +Lorem ipsum dolor sit amet [^19] + +Lorem ipsum dolor sit amet [^20] + +Lorem ipsum dolor sit amet [^21] [^22] + +Lorem ipsum dolor sit amet [^23] + +Lorem ipsum dolor sit amet [^24] + +Lorem ipsum dolor sit amet [^25] + +Lorem ipsum dolor sit amet [^26] + +Lorem ipsum dolor sit amet [^27] + +Lorem ipsum dolor sit amet [^28] + +Lorem ipsum dolor sit amet [^29] + +Lorem ipsum dolor sit amet [^30] + +Lorem ipsum dolor sit amet [^31] + +Lorem ipsum dolor sit amet [^32] + +Lorem ipsum dolor sit amet [^33] + +Lorem ipsum dolor sit amet [^34] + +Lorem ipsum dolor sit amet [^35] + +Lorem ipsum dolor sit amet [^36] + +Lorem ipsum dolor sit amet [^37] + +Lorem ipsum dolor sit amet [^38] + +Lorem ipsum dolor sit amet [^39] + +Lorem ipsum dolor sit amet [^40] + +Lorem ipsum dolor sit amet [^41] + +Lorem ipsum dolor sit amet [^42] + +Lorem ipsum dolor sit amet [^43] + +Lorem ipsum dolor sit amet [^44] + +Lorem ipsum dolor sit amet [^45] + +Lorem ipsum dolor sit amet [^46] + +Lorem ipsum dolor sit amet [^47] + +Lorem ipsum dolor sit amet [^48] + +Lorem ipsum dolor sit amet [^49] [^50] + +[^1]: footnote 1 +[^2]: footnote 2 +[^3]: footnote 3 +[^4]: footnote 4 +[^5]: footnote 5 +[^6]: footnote 6 +[^7]: footnote 7 +[^8]: footnote 8 +[^9]: footnote 9 +[^10]: footnote 10 +[^11]: footnote 11 +[^12]: footnote 12 +[^13]: footnote 13 +[^14]: footnote 14 +[^15]: footnote 15 +[^16]: footnote 16 +[^17]: footnote 17 +[^18]: footnote 18 +[^19]: footnote 19 +[^20]: footnote 20 +[^21]: footnote 21 +[^22]: footnote 22 +[^23]: footnote 23 +[^24]: footnote 24 +[^25]: footnote 25 +[^26]: footnote 26 +[^27]: footnote 27 +[^28]: footnote 28 +[^29]: footnote 29 +[^30]: footnote 30 +[^31]: footnote 31 +[^32]: footnote 32 +[^33]: footnote 33 +[^34]: footnote 34 +[^35]: footnote 35 +[^36]: footnote 36 +[^37]: footnote 37 +[^38]: footnote 38 +[^39]: footnote 39 +[^40]: footnote 40 +[^41]: footnote 41 +[^42]: footnote 42 +[^43]: footnote 43 +[^44]: footnote 44 +[^45]: footnote 45 +[^46]: footnote 46 +[^47]: footnote 47 +[^48]: footnote 48 +[^49]: footnote 49 +[^50]: footnote 50