fix(docs): ref scroll (#29819)

Fix various scrolling bugs with reference pages:

- Take out smooth scrolling as it tends to lead to buggy scrolling position
- Add overscroll-behavior: contain to the nav, otherwise overscrolling on the nav scrolls the main page, which causes the nav to jump around unexpectedly
- Put aria-current calculation in useEffect as otherwise it doesn't trigger properly on first load
This commit is contained in:
Charis authored and GitHub committed 2024-10-11 15:51:51 +00:00
1 parent e7cdc04dc6
commit 1236406e88
3 files changed
+51 -17

No files matched your search

@@ -11,9 +11,9 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, use
import { cn } from 'ui'
import { BASE_PATH } from '~/lib/constants'
import type { AbbrevApiReferenceSection } from '~/features/docs/Reference.utils'
import { isElementInViewport } from '~/features/ui/helpers.dom'
import { BASE_PATH } from '~/lib/constants'
export const ReferenceContentInitiallyScrolledContext = createContext<boolean>(false)
@@ -27,27 +27,27 @@ export function ReferenceContentScrollHandler({
version: string
isLatestVersion: boolean
}>) {
const checkedPathnameOnLoad = useRef(false)
const [initiallyScrolled, setInitiallyScrolled] = useState(false)
const pathname = usePathname()
useEffect(() => {
if (!checkedPathnameOnLoad.current) {
if (!initiallyScrolled) {
const initialSelectedSection = pathname.replace(
`/reference/${libPath}/${isLatestVersion ? '' : `${version}/`}`,
''
)
if (initialSelectedSection) {
const section = document.getElementById(initialSelectedSection)
section?.scrollIntoView()
section?.querySelector('h2')?.focus()
if (section) {
window.scrollTo(0, section.offsetTop - 60 /* space for header + padding */)
section.querySelector('h2')?.focus()
}
}
checkedPathnameOnLoad.current = true
setInitiallyScrolled(true)
}
}, [pathname, libPath, version, isLatestVersion])
}, [pathname, libPath, version, isLatestVersion, initiallyScrolled])
return (
<ReferenceContentInitiallyScrolledContext.Provider value={initiallyScrolled}>
@@ -60,14 +60,33 @@ export function ReferenceNavigationScrollHandler({
children,
...rest
}: PropsWithChildren & HTMLAttributes<HTMLDivElement>) {
const parentRef = useRef<HTMLElement>()
const ref = useRef<HTMLDivElement>()
const initialScrollHappened = useContext(ReferenceContentInitiallyScrolledContext)
useEffect(() => {
if (!ref.current) return
let scrollingParent: HTMLElement = ref.current
while (scrollingParent && !(scrollingParent.scrollHeight > scrollingParent.clientHeight)) {
scrollingParent = scrollingParent.parentElement
}
parentRef.current = scrollingParent
}, [])
const scrollActiveIntoView = useCallback(() => {
const currentLink = ref.current?.querySelector('[aria-current=page]') as HTMLElement
if (currentLink && !isElementInViewport(currentLink)) {
currentLink.scrollIntoView({
block: 'center',
// Calculate the offset of the current link relative to scrollingParent
// and scroll the parent to the top of the link.
const offsetTop = currentLink.offsetTop
const parentOffsetTop = parentRef.current?.offsetTop ?? 0
const scrollPosition = offsetTop - parentOffsetTop
parentRef.current?.scrollTo({
top: scrollPosition - 60 /* space for header + padding */,
})
}
}, [])
@@ -122,11 +141,8 @@ function createReferenceSubsectionNavigator(href: string, sectionSlug?: string)
evt.preventDefault()
history.pushState({}, '', `${BASE_PATH}${href}`)
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
const domElement = document.getElementById(sectionSlug)
domElement?.scrollIntoView({
behavior: reduceMotion ? 'auto' : 'smooth',
})
domElement?.scrollIntoView()
domElement?.querySelector('h2')?.focus()
}
}
@@ -141,8 +157,13 @@ export function RefInternalLink({
sectionSlug?: string
children: React.ReactNode
}) {
const onClick = useCallback(
(evt: MouseEvent) => createReferenceSubsectionNavigator(href, sectionSlug)(evt),
[href, sectionSlug]
)
return (
<Link href={href} onClick={createReferenceSubsectionNavigator(href, sectionSlug)}>
<Link href={href} onClick={onClick}>
{children}
</Link>
)
@@ -166,6 +187,18 @@ export function RefLink({
const isActive =
pathname === href || (pathname === basePath && href.replace(basePath, '') === '/introduction')
useEffect(() => {
if (ref.current) {
ref.current.ariaCurrent = isActive ? 'page' : undefined
ref.current.className = getLinkStyles(isActive, className)
}
}, [isActive, className])
const onClick = useCallback(
(evt: MouseEvent) => createReferenceSubsectionNavigator(href, section.slug)(evt),
[href, section.slug]
)
if (!('title' in section)) return null
const isCompoundSection = !skipChildren && 'items' in section && section.items.length > 0
@@ -181,9 +214,8 @@ export function RefLink({
// prefetching just wastes bandwidth
prefetch={false}
href={href}
aria-current={isActive ? 'page' : false}
className={getLinkStyles(isActive, className)}
onClick={createReferenceSubsectionNavigator(href, section.slug)}
onClick={onClick}
>
{section.title}
</Link>
+2 -1
View File
@@ -100,7 +100,7 @@ export function StickyHeader({ title, monoFont = false, className }: StickyHeade
<h2
tabIndex={-1} // For programmatic focus on auto-scroll to section
className={cn(
'sticky top-0 z-10',
'sticky top-0 z-[1]',
'w-full',
// Enough padding to cover the background when stuck to the top,
// then readjust with negative margin to prevent it looking too
@@ -111,6 +111,7 @@ export function StickyHeader({ title, monoFont = false, className }: StickyHeade
'bg-gradient-to-b from-background from-85% to-transparent to-100%',
'text-2xl font-medium text-foreground',
'scroll-mt-[calc(var(--header-height)+1rem)]',
'focus:outline-none',
monoFont && 'font-mono',
className
)}
+1
View File
@@ -300,6 +300,7 @@ const NavContainer = memo(function NavContainer({ children }: PropsWithChildren)
'relative lg:sticky',
'w-full lg:w-auto',
'h-fit lg:h-screen overflow-y-scroll lg:overflow-auto',
'[overscroll-behavior:contain]',
'backdrop-blur backdrop-filter bg-background',
'flex flex-col flex-grow'
)}