Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 53 additions & 3 deletions src/components/Layout/Toc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,56 @@ import cx from 'classnames';
import {useTocHighlight} from './useTocHighlight';
import type {Toc} from '../MDX/TocContext';

let animationFrameId: number;

const smoothScrollTo = (targetId: string) => {
const element = document.getElementById(targetId);
if (!element) return;

(window as any).__isAutoScrolling = true;

const headerOffset = 84;
const startPosition = window.scrollY;
const elementPosition = element.getBoundingClientRect().top;
const targetPosition = startPosition + elementPosition - headerOffset;
const distance = targetPosition - startPosition;
const duration = 300;
let startTime: number | null = null;

const easeInOutCubic = (t: number): number => {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
};

const animation = (currentTime: number) => {
if (startTime === null) startTime = currentTime;
const timeElapsed = currentTime - startTime;
const progress = Math.min(timeElapsed / duration, 1);
const easedProgress = easeInOutCubic(progress);

window.scrollTo(0, startPosition + distance * easedProgress);

if (progress < 1) {
animationFrameId = requestAnimationFrame(animation);
} else {
(window as any).__isAutoScrolling = false;
}
};

cancelAnimationFrame(animationFrameId);
animationFrameId = requestAnimationFrame(animation);
};

const getAnchorIdFromUrl = (url: string) => {
return url.startsWith('#') ? url.substring(1) : url;
};

export function Toc({headings}: {headings: Toc}) {
const {currentIndex} = useTocHighlight();
// TODO: We currently have a mismatch between the headings in the document
// and the headings we find in MarkdownPage (i.e. we don't find Recap or Challenges).
// Select the max TOC item we have here for now, but remove this after the fix.
const selectedIndex = Math.min(currentIndex, headings.length - 1);

return (
<nav role="navigation" className="pt-20 sticky top-0 end-0">
{headings.length > 0 && (
Expand All @@ -37,11 +81,12 @@ export function Toc({headings}: {headings: Toc}) {
if (!h.url && process.env.NODE_ENV === 'development') {
console.error('Heading does not have URL');
}
const anchorId = getAnchorIdFromUrl(h.url);
return (
<li
key={`heading-${h.url}-${i}`}
className={cx(
'text-sm px-2 rounded-s-xl',
'text-sm px-2 rounded-s-xl transition-colors duration-200 ease-in-out',
selectedIndex === i
? 'bg-highlight dark:bg-highlight-dark'
: null,
Expand All @@ -55,9 +100,14 @@ export function Toc({headings}: {headings: Toc}) {
selectedIndex === i
? 'text-link dark:text-link-dark font-bold'
: 'text-secondary dark:text-secondary-dark',
'block hover:text-link dark:hover:text-link-dark leading-normal py-2'
'block hover:text-link dark:hover:text-link-dark leading-normal py-2 transition-colors duration-200 ease-in-out'
)}
href={h.url}>
href={h.url}
onClick={(e) => {
e.preventDefault();
smoothScrollTo(anchorId);
window.history.pushState(null, '', `#${anchorId}`);
}}>
{h.text}
</a>
</li>
Expand Down
3 changes: 3 additions & 0 deletions src/components/Layout/useTocHighlight.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export function useTocHighlight() {
}

function throttledUpdateActiveLink() {
if ((window as any).__isAutoScrolling) {
return;
}
if (timeoutRef.current === null) {
timeoutRef.current = window.setTimeout(() => {
timeoutRef.current = null;
Expand Down