52 lines
1.9 KiB
JavaScript
52 lines
1.9 KiB
JavaScript
import { useEffect } from 'react'
|
|
import { useLocation } from 'react-router-dom'
|
|
|
|
export default function ScrollToTop() {
|
|
const { pathname, hash } = useLocation()
|
|
|
|
// Cross-page navigation: scroll to hash or top on route change.
|
|
//
|
|
// getElementById, not querySelector. querySelector THROWS on a fragment that
|
|
// is not a valid CSS selector, and an id starting with a digit is not one:
|
|
// `#8x8-implementation` and even `/#1` would throw here, in an effect inside
|
|
// the root route, and the error boundary would replace the header, the page
|
|
// and the footer with the error screen. The long-form service pages exist to
|
|
// be deep-linked from search results and AI answers, so a link nobody here
|
|
// wrote must never be able to blank the page. An unknown id scrolls to top.
|
|
useEffect(() => {
|
|
if (hash.length > 1) {
|
|
let el = null
|
|
try {
|
|
el = document.getElementById(decodeURIComponent(hash.slice(1)))
|
|
} catch {
|
|
el = null // a fragment that is not valid percent-encoding
|
|
}
|
|
if (el) {
|
|
el.scrollIntoView({ behavior: 'smooth' })
|
|
return
|
|
}
|
|
}
|
|
window.scrollTo(0, 0)
|
|
}, [pathname, hash])
|
|
|
|
// Same-page: React Router won't re-navigate if URL is already identical,
|
|
// so intercept clicks on any link pointing to #contact-form directly.
|
|
useEffect(() => {
|
|
const handleClick = (e) => {
|
|
const anchor = e.target.closest('a')
|
|
if (!anchor) return
|
|
const href = anchor.getAttribute('href') || ''
|
|
if (!href.includes('#contact-form')) return
|
|
const el = document.querySelector('#contact-form')
|
|
if (!el) return
|
|
e.preventDefault()
|
|
el.scrollIntoView({ behavior: 'smooth' })
|
|
window.history.pushState(null, '', '#contact-form')
|
|
}
|
|
document.addEventListener('click', handleClick)
|
|
return () => document.removeEventListener('click', handleClick)
|
|
}, [])
|
|
|
|
return null
|
|
}
|