diff --git a/dotcom-rendering/src/components/TableOfContents.importable.tsx b/dotcom-rendering/src/components/TableOfContents.importable.tsx index 048b9c055a5..24b412f3d09 100644 --- a/dotcom-rendering/src/components/TableOfContents.importable.tsx +++ b/dotcom-rendering/src/components/TableOfContents.importable.tsx @@ -9,7 +9,7 @@ import { SvgChevronDownSingle, SvgChevronUpSingle, } from '@guardian/source/react-components'; -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { ArticleDisplay, type ArticleFormat } from '../lib/articleFormat'; import { getZIndex } from '../lib/getZIndex'; import type { TableOfContentsItem } from '../model/enhanceTableOfContents'; @@ -75,7 +75,7 @@ const detailsStyles = css` `; const stickyStyles = css` position: sticky; - top: 0; + top: -1px; background: ${palette('--article-background')}; z-index: ${getZIndex('tableOfContents')}; summary { @@ -137,14 +137,39 @@ const verticalStyle = css` export const TableOfContents = ({ tableOfContents, format }: Props) => { const [open, setOpen] = useState(tableOfContents.length < 5); + const tocRef = useRef(null); + + // Automatically collapse the ToC when it becomes sticky (i.e. when it reaches the top of the viewport). This + // approach is inspired by: + // https://css-tricks.com/how-to-detect-when-a-sticky-element-gets-pinned/ + useEffect(() => { + const tocElement = tocRef.current; + + if (!tocElement) return; + + const observer = new IntersectionObserver( + ([e]) => { + // Verify whether the ToC is at the top of the viewport or the bottom. It should only collapse when it + // reaches the top. + if (e && e.boundingClientRect.top < 0) { + setOpen(false); + } + }, + { threshold: [1] }, + ); + + observer.observe(tocElement); + + return () => { + observer.disconnect(); + }; + }, []); return (
5 ? stickyStyles : undefined, - ]} + css={[detailsStyles, stickyStyles]} data-component="table-of-contents" >