From 6d76c1f16660b4906205666d43a848c97b39b7c6 Mon Sep 17 00:00:00 2001 From: Shashank RM Date: Tue, 20 May 2025 17:20:17 +0530 Subject: [PATCH] fix(docs): improve TOC anchor navigation for Safari/mobile - Ensure TOC closes before scrolling on mobile/tablet, with delay for smooth Safari behavior - Improves anchor navigation reliability when TOC is toggled via button on small screens - No functional change for desktop/large screens --- site/src/pages/docs/TableOfContents.tsx | 78 +++++++++++++++++++++---- 1 file changed, 66 insertions(+), 12 deletions(-) diff --git a/site/src/pages/docs/TableOfContents.tsx b/site/src/pages/docs/TableOfContents.tsx index 80150c3..e3e7835 100644 --- a/site/src/pages/docs/TableOfContents.tsx +++ b/site/src/pages/docs/TableOfContents.tsx @@ -1,52 +1,74 @@ +import React, { useCallback, useRef, useState } from 'react'; import { FaAngleDown } from 'react-icons/fa6'; import { HeadingItem, useHeadings } from '../../hooks/useHeadings'; import { cn } from '../../utils/cn'; export default function TableOfContents() { + const [open, setOpen] = useState(false); + const navRef = useRef(null); + + // Close TOC when clicking outside (mobile only) + React.useEffect(() => { + if (!open) return; + function handleClick(e: MouseEvent) { + if (navRef.current && !navRef.current.contains(e.target as Node)) { + setOpen(false); + } + } + document.addEventListener('mousedown', handleClick); + return () => document.removeEventListener('mousedown', handleClick); + }, [open]); + return ( -
+
); } -function HeadingList() { +function HeadingList({ tocOpen, setTocOpen }: { tocOpen?: boolean; setTocOpen?: (v: boolean) => void }) { const headings = useHeadings(); - return (
    {headings.map(({ id, title, items }) => (
  • - - {items.length > 0 && } + + {items.length > 0 && }
  • ))}
); } -function NestedHeadingList({ items }: { items: HeadingItem[] }) { +function NestedHeadingList({ items, setTocOpen }: { items: HeadingItem[]; setTocOpen?: (v: boolean) => void }) { return (
    {items.map((item) => { return (
  • - +
  • ); })} @@ -54,9 +76,41 @@ function NestedHeadingList({ items }: { items: HeadingItem[] }) { ); } -function NavLink({ id, title }: HeadingItem) { +function NavLink({ id, title, setTocOpen }: HeadingItem & { setTocOpen?: (v: boolean) => void }) { + const handleClick = useCallback( + (e: React.MouseEvent) => { + // Use querySelector instead of getElementById for anchor navigation + const el = document.querySelector(`#${CSS.escape(id)}`); + // Detect mobile/tablet by checking if setTocOpen exists (only passed on mobile) + const isMobile = !!setTocOpen && window.innerWidth < 1280; + if (el) { + // Safari smooth scroll workaround + const supportsSmoothScroll = 'scrollBehavior' in document.documentElement.style; + if (supportsSmoothScroll) { + e.preventDefault(); + if (isMobile && setTocOpen) { + setTocOpen(false); + setTimeout(() => { + (el as HTMLElement).scrollIntoView({ behavior: 'smooth', block: 'start' }); + history.replaceState(null, '', `#${id}`); + }, 250); // Wait for TOC to close/transition + } else { + (el as HTMLElement).scrollIntoView({ behavior: 'smooth', block: 'start' }); + history.replaceState(null, '', `#${id}`); + } + } + // else: let default anchor behavior happen + } + // else: let default anchor behavior happen + }, + [id, setTocOpen] + ); return ( - + {title} );