Skip to content

Commit a73c094

Browse files
committed
refactor(toc): replace broken offset-based scroll spy with IntersectionObserver 🎯
Removed fragile offsetTop calculations and magic numbers. Now using IntersectionObserver API for reliable viewport detection that handles resize, fold/unfold, and dynamic content changes gracefully. - ✨ New useHeadingScrollSpy hook with IntersectionObserver - 🗑️ Removed stale offsetTop logic from useToc - 🎨 React-driven focusSight state via Zustand store - 🚀 Auto-scroll TOC when focused heading changes - 🧹 Deleted old useScrollSyncToc hook No more edge cases! Works perfectly when editor size changes. 🎉
1 parent b8501a5 commit a73c094

File tree

10 files changed

+331
-78
lines changed

10 files changed

+331
-78
lines changed

packages/webapp/src/components/pages/document/components/DesktopEditor.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { useCallback, useRef } from 'react'
22
import ToolbarDesktop from '@components/TipTap/toolbar/ToolbarDesktop'
33
import EditorContent from './EditorContent'
44
import TOC from './Toc'
5-
import { useAdjustEditorSizeForChatRoom, useTOCResize, useScrollSyncToc } from '../hooks'
5+
import { useAdjustEditorSizeForChatRoom, useTOCResize } from '../hooks'
66
import useUpdateDocPageUnreadMsg from '../hooks/useUpdateDocPageUnreadMsg'
77
import { Chatroom } from '@components/chatroom'
88
import { HoverMenu } from '@components/ui/HoverMenu'
99
import { useMessageFeedContext } from '@components/chatroom/components/MessageFeed/MessageFeedContext'
10+
import { useHeadingScrollSpy } from '@components/toc/hooks'
1011

1112
const MessageHoverMenu = (props: React.ComponentProps<typeof HoverMenu>) => {
1213
const { virtualizerRef, messageContainerRef } = useMessageFeedContext()
@@ -36,8 +37,8 @@ const DesktopEditor = () => {
3637

3738
useUpdateDocPageUnreadMsg()
3839

39-
// Use the custom hook for scroll sync -> TOC
40-
useScrollSyncToc(editorWrapperRef)
40+
// IntersectionObserver-based scroll spy for TOC highlighting
41+
useHeadingScrollSpy(editorWrapperRef)
4142

4243
return (
4344
<>
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export * from './useAdjustEditorSizeForChatRoom'
22
export * from './useTOCResize'
3-
export { default as useScrollSyncToc } from './useScrollSyncToc'
43
export { default as useCopyDocumentToClipboard } from './useCopyDocumentToClipboard'
54
export { default as useClipboard } from './useClipboard'

packages/webapp/src/components/pages/document/hooks/useScrollSyncToc.tsx

Lines changed: 0 additions & 56 deletions
This file was deleted.

packages/webapp/src/components/toc/TocDesktop.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useRef, useState, useCallback } from 'react'
2-
import { useToc } from './hooks'
2+
import { useToc, useTocAutoScroll } from './hooks'
33
import { buildNestedToc } from './utils'
44
import { TocHeader } from './TocHeader'
55
import { TocItem } from './TocItem'
@@ -21,6 +21,9 @@ function removeContextMenuActiveClass() {
2121
export function TocDesktop({ className = '' }: TocDesktopProps) {
2222
const { items, toggleSection } = useToc()
2323
const contextMenuRef = useRef<HTMLDivElement>(null)
24+
25+
// Auto-scroll TOC when focused heading changes
26+
useTocAutoScroll()
2427
const [contextMenuState, setContextMenuState] = useState<{
2528
headingId: string | null
2629
isOpen: boolean

packages/webapp/src/components/toc/TocItem.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import { useCallback } from 'react'
22
import { CaretRight, ChatLeft } from '@icons'
33
import { useStore, useChatStore } from '@stores'
44
import type { TocItem as TocItemType } from '@types'
5-
import { useTocActions, usePresentUsers, useUnreadCount, useActiveHeading } from './hooks'
5+
import {
6+
useTocActions,
7+
usePresentUsers,
8+
useUnreadCount,
9+
useActiveHeading,
10+
useFocusedHeadingStore
11+
} from './hooks'
612
import { scrollToHeading, buildNestedToc } from './utils'
713
import { useModal } from '@components/ui/ModalDrawer'
814
import AvatarStack from '@components/AvatarStack'
@@ -20,6 +26,10 @@ export function TocItem({ item, children, variant, onToggle }: TocItemProps) {
2026
editor: { instance: editor }
2127
} = useStore((state) => state.settings)
2228

29+
// Use the scroll spy store to determine if this heading is in focus
30+
const focusedHeadingId = useFocusedHeadingStore((s) => s.focusedHeadingId)
31+
const isFocused = focusedHeadingId === item.id
32+
2333
const presentUsers = usePresentUsers(item.id)
2434
const [, setActiveHeading] = useActiveHeading()
2535
const unreadCount = useUnreadCount(item.id)
@@ -71,9 +81,8 @@ export function TocItem({ item, children, variant, onToggle }: TocItemProps) {
7181

7282
return (
7383
<li
74-
className={`toc__item relative w-full ${!item.open ? 'closed' : ''}`}
75-
data-id={item.id}
76-
data-offsettop={item.offsetTop}>
84+
className={`toc__item relative w-full ${!item.open ? 'closed' : ''} ${isFocused ? 'focusSight' : ''}`}
85+
data-id={item.id}>
7786
<a
7887
className={`group relative ${isActive ? 'active activeTocBorder bg-gray-300' : ''} ${variant === 'mobile' ? '!py-2' : ''} ${variant === 'mobile' && item.level === 1 ? 'ml-3' : ''}`}
7988
onClick={handleClick}

packages/webapp/src/components/toc/hooks/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,10 @@ export { useTocActions } from './useTocActions'
66
export { useActiveHeading } from './useActiveHeading'
77
export { usePresentUsers } from './usePresentUsers'
88
export { useUnreadCount } from './useUnreadCount'
9+
10+
// Scroll spy hooks
11+
export {
12+
useHeadingScrollSpy,
13+
useTocAutoScroll,
14+
useFocusedHeadingStore
15+
} from './useHeadingScrollSpy'

0 commit comments

Comments
 (0)