Skip to content

Commit

Permalink
fix: fast outline scroll
Browse files Browse the repository at this point in the history
  • Loading branch information
jxom committed Nov 9, 2023
1 parent d049ee3 commit 71ac14b
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 0 deletions.
38 changes: 38 additions & 0 deletions src/app/components/Outline.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Fragment, useEffect, useMemo, useState } from 'react'
import { useLocation } from 'react-router-dom'

import { debounce } from '../utils/debounce.js'
import * as styles from './Outline.css.js'
import { root as Heading, slugTarget } from './mdx/Heading.css.js'

type OutlineItems = {
id: string
level: number
slugTargetElement: Element
topOffset: number
text: string | null
}[]

Expand Down Expand Up @@ -42,9 +44,12 @@ export function Outline({
const slugTargetElement = element.querySelector(`.${slugTarget}`)
if (!slugTargetElement) return null

const box = slugTargetElement.getBoundingClientRect()

const id = slugTargetElement.id
const level = Number(element.tagName[1])
const text = element.textContent
const topOffset = window.scrollY + box.top

if (level < minLevel || level > maxLevel) return null

Expand All @@ -53,6 +58,7 @@ export function Outline({
level,
slugTargetElement,
text,
topOffset,
}
})
.filter(Boolean) as OutlineItems
Expand Down Expand Up @@ -103,6 +109,38 @@ export function Outline({
return () => observer.disconnect()
}, [items])

// Intersection observers are a bit unreliable for fast scrolling,
// use scroll event listener to sync active item.
useEffect(() => {
if (typeof window === 'undefined') return

const callback = debounce(() => {
if (window.scrollY === 0) {
setActiveId(items[0].id)
return
}

if (
window.scrollY + document.documentElement.clientHeight >=
document.documentElement.scrollHeight
) {
setActiveId(items[items.length - 1].id)
return
}

for (let i = 0; i < items.length; i++) {
const item = items[i]
if (window.scrollY < item.topOffset) {
setActiveId(items[i - 1]?.id)
break
}
}
}, 100)

window.addEventListener('scroll', callback)
return () => window.removeEventListener('scroll', callback)
}, [items])

if (items.length === 0) return null

const levelItems = items.filter((item) => item.level === minLevel)
Expand Down
10 changes: 10 additions & 0 deletions src/app/utils/debounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function debounce(fn: () => void, delay: number): () => void {
let invoked = false
return () => {
invoked = true
setTimeout(() => {
if (invoked) fn()
invoked = false
}, delay)
}
}

0 comments on commit 71ac14b

Please sign in to comment.