Files
Anthony Lio 27a4421d6e fix(docs): stop sidebar from jumping on scroll (#49807)
## What kind of change does this PR introduce?

bug fix to stop sidebar jump on scroll within docs

## What is the current behavior?

the docs sidebar shifts up by 1px as soon as scrolling start as the top
bar height include bottom border causing the jump as height token differ
from the whole height

## What is the new behavior?

favor box shadow instead of a border for the bottom line, so height
matches the token and nothing needs to compensate any more which allows
to remove some `+1px` elsewhere

+ also drops a nested `lg:sticky` in the sidebar that did nothing inside
an already-sticky parent

| state | preview |
| -------|------|
| before | <video
src="https://github.com/user-attachments/assets/b4bbfa2d-6595-4711-bb2b-bd2bf3aded8a"
/> |
| after | <video
src="https://github.com/user-attachments/assets/f044aebc-ef14-42c1-8564-3b290399d00b"
/> |

## Additional context

- header now uses the existing `subhighlight-border` utility, which was
not used anywhere else it seems, could also be renamed?
- could be down the other way by keeping border and fixing the jump

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved documentation navigation alignment by removing unnecessary
spacing from sticky sidebars, table of contents, and section headings.
* Updated desktop navigation behavior for more consistent scrolling and
viewport layout.
* Refined the top navigation bar’s border styling for a cleaner
appearance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-01 22:04:41 +03:00

190 lines
5.3 KiB
TypeScript

import { useNavigationMenuContext } from '~/components/Navigation/NavigationMenu/NavigationMenu.Context'
import { menuState } from '~/hooks/useMenuState'
import { safeHistoryReplaceState } from '~/lib/historyUtils'
import { useRouter } from 'next/compat/router'
import Image from 'next/legacy/image'
import { FC, PropsWithChildren } from 'react'
import { useInView } from 'react-intersection-observer'
import { cn, highlightSelectedNavItem } from 'ui'
interface ISectionContainer {
id: string
title?: string
monoFont?: boolean
slug: string
scrollSpyHeader?: boolean
singleColumn?: boolean
icon?: string
}
type RefSubLayoutSubComponents = {
Section: FC<PropsWithChildren<ISectionContainer>>
EducationSection: FC<PropsWithChildren<IEducationSection>>
EducationRow: FC<PropsWithChildren<IEducationRow>>
Details: FC<ISectionDetails>
Examples: FC<ISectionExamples>
}
type StickyHeader = {
id: string
slug?: string
title?: string
monoFont?: boolean
scrollSpyHeader?: boolean // whether or not the header updates the url on scroll
icon?: string
}
type RefSubLayoutType = {}
interface IEducationRow {
className?: string
}
interface IEducationSection {
id: string
title?: string
monoFont?: boolean
slug: string
scrollSpyHeader?: boolean
hideTitle?: boolean
icon?: string
}
interface ISectionDetails {}
interface ISectionExamples {}
const RefSubLayout: FC<PropsWithChildren<RefSubLayoutType>> & RefSubLayoutSubComponents = (
props
) => {
return (
<div className="flex flex-col w-full divide-y px-5 max-w-7xl mx-auto py-16">
{props.children}
</div>
)
}
const Section: FC<PropsWithChildren<ISectionContainer>> = (props) => {
return (
<article
key={props.id + 'section'}
className={[
props.singleColumn ? 'prose w-full' : 'w-full',
'py-16 first:pt-8 last:pb-8',
].join(' ')}
>
<StickyHeader {...props} />
<div
className={`ref-container w-full gap-16 ${
!props.singleColumn ? 'grid lg:grid-cols-2' : 'ref-container--full-width lg:max-w-3xl'
}`}
>
{props.children}
</div>
</article>
)
}
const StickyHeader: FC<StickyHeader> = ({ icon, ...props }) => {
const router = useRouter()
const { setActiveRefItem } = useNavigationMenuContext()
// we're serving search bots a different file (/crawlers/[...slug])
// and need to modify content to suit that
const isCrawlerPage = router?.route.includes('/crawlers/[...slug]') || false
const { ref } = useInView({
threshold: 1,
rootMargin: '30% 0% -35% 0px',
onChange: (inView, entry) => {
if (inView && window) highlightSelectedNavItem(entry.target.attributes['data-ref-id'].value)
if (inView && props.scrollSpyHeader) {
safeHistoryReplaceState(entry.target.id)
// if (setActiveRefItem) setActiveRefItem(entry.target.attributes['data-ref-id'].value)
menuState.setMenuActiveRefId(entry.target.attributes['data-ref-id'].value)
// router.push(`/reference/javascript/${entry.target.attributes['data-ref-id'].value}`, null, {
// shallow: true,
// })
}
},
})
return (
<div className={['flex items-center gap-3 not-prose', icon && 'mb-8'].join(' ')}>
{icon && (
<div className="w-8 h-8 bg-brand-300 rounded-sm flex items-center justify-center">
<Image width={16} height={16} alt={icon} src={`${icon}.svg`} />
</div>
)}
{isCrawlerPage ? (
<h1>{props.title}</h1>
) : (
<h2
ref={ref}
id={props.slug}
data-ref-id={props.id}
className={cn(
'text-2xl font-medium text-foreground scroll-mt-[calc(32px+2rem)] lg:scroll-mt-[calc(var(--header-height)+4rem)]',
!icon && 'mb-8',
props.monoFont && 'font-mono'
)}
>
{props.title && <span className="max-w-xl">{props.title}</span>}
</h2>
)}
</div>
)
}
const Details: FC<PropsWithChildren<ISectionDetails>> = (props) => {
/**
* `min-w` is necessary because these are used as grid children, which have
* default `min-w-auto`
*/
return <div className="relative w-full min-w-full">{props.children}</div>
}
const Examples: FC<PropsWithChildren<ISectionExamples>> = (props) => {
/**
* `min-w` is necessary because these are used as grid children, which have
* default `min-w-auto`
*/
return (
<div className="w-full min-w-full">
<div className="sticky top-32">{props.children}</div>
</div>
)
}
const EducationRow: FC<PropsWithChildren<IEducationRow>> = (props) => {
return (
<div className={['grid lg:grid-cols-2 gap-8 lg:gap-16', props.className].join(' ')}>
{props.children}
</div>
)
}
const EducationSection: FC<PropsWithChildren<IEducationSection>> = ({
icon,
hideTitle = false,
...props
}) => {
return (
<article
key={props.id + 'education'}
className={'prose max-w-none py-16 first:pb-8 first:pt-0 last:pb-8'}
>
{!hideTitle && <StickyHeader {...props} icon={icon} />}
{props.children}
</article>
)
}
// function based layout
RefSubLayout.Section = Section
// education based layout
RefSubLayout.EducationSection = EducationSection
RefSubLayout.EducationRow = EducationRow
// common columns
RefSubLayout.Details = Details
RefSubLayout.Examples = Examples
export default RefSubLayout