docs: DX and performance improvements in API reference (#9430)

- Improve scroll behavior between active sections
- Improve lag when clicking on a sidebar item
- Refactor internal working of the `SidebarProvider` to find active items faster.
- Use Next.js's `useRouter` hook for changing the hash (since they added the option to disable scroll)
- Change `isBrowser` from a hook to a provider since it's widely used across applications.
- Other general improvements and fixes.

Closes DOCS-952
This commit is contained in:
Shahed Nasser
2024-10-02 18:51:02 +03:00
committed by GitHub
parent 5fb9c1f82e
commit 0f6f56e764
38 changed files with 294 additions and 218 deletions

View File

@@ -2,6 +2,7 @@
import clsx from "clsx"
import { useActiveOnScroll, useSidebar } from "docs-ui"
import { useRouter } from "next/navigation"
import { useEffect, useRef } from "react"
export type SectionProps = {
@@ -20,6 +21,7 @@ const Section = ({
useDefaultIfNoActive: false,
})
const { setActivePath } = useSidebar()
const router = useRouter()
useEffect(() => {
if ("scrollRestoration" in history) {
@@ -30,7 +32,9 @@ const Section = ({
useEffect(() => {
if (activeItemId.length) {
history.pushState({}, "", `#${activeItemId}`)
router.push(`#${activeItemId}`, {
scroll: false,
})
setActivePath(activeItemId)
}
}, [activeItemId])

View File

@@ -12,6 +12,7 @@ import type { TagOperationCodeSectionProps } from "./CodeSection"
import TagsOperationDescriptionSection from "./DescriptionSection"
import DividedLayout from "@/layouts/Divided"
import { useLoading } from "@/providers/loading"
import { useRouter } from "next/navigation"
import SectionDivider from "../../Section/Divider"
import checkElementInViewport from "../../../utils/check-element-in-viewport"
@@ -33,7 +34,8 @@ const TagOperation = ({
endpointPath,
className,
}: TagOperationProps) => {
const { setActivePath } = useSidebar()
const { activePath, setActivePath } = useSidebar()
const router = useRouter()
const [show, setShow] = useState(false)
const path = useMemo(
() => getSectionId([...(operation.tags || []), operation.operationId]),
@@ -57,10 +59,14 @@ const TagOperation = ({
}
setShow(true)
}
// can't use next router as it doesn't support
// changing url without scrolling
history.replaceState({}, "", `#${path}`)
setActivePath(path)
if (location.hash !== path) {
router.push(`#${path}`, {
scroll: false,
})
}
if (activePath !== path) {
setActivePath(path)
}
}
},
})
@@ -77,7 +83,7 @@ const TagOperation = ({
)
const scrollIntoView = useCallback(() => {
if (nodeRef.current && !checkElementInViewport(nodeRef.current, 10)) {
if (nodeRef.current && !checkElementInViewport(nodeRef.current, 0)) {
const elm = nodeRef.current as HTMLElement
scrollToTop(
elm.offsetTop + (elm.offsetParent as HTMLElement)?.offsetTop,

View File

@@ -72,7 +72,7 @@ const TagSectionSchema = ({ schema, tagName }: TagSectionSchemaProps) => {
useEffect(() => {
if (schemaSlug === (activePath || location.hash.replace("#", ""))) {
const elm = document.getElementById(schemaSlug) as HTMLElement
if (!checkElementInViewport(elm, 40)) {
if (!checkElementInViewport(elm, 0)) {
scrollToElement(elm)
}
}
@@ -85,7 +85,7 @@ const TagSectionSchema = ({ schema, tagName }: TagSectionSchemaProps) => {
const section = entry.target
if (
(inView || checkElementInViewport(section, 40)) &&
(inView || checkElementInViewport(section, 10)) &&
activePath !== schemaSlug
) {
// can't use next router as it doesn't support

View File

@@ -20,7 +20,7 @@ import { useArea } from "@/providers/area"
import SectionDivider from "../../Section/Divider"
import clsx from "clsx"
import { Feedback, Loading, Link } from "docs-ui"
import { usePathname } from "next/navigation"
import { usePathname, useRouter } from "next/navigation"
import formatReportLink from "@/utils/format-report-link"
import { SchemaObject, TagObject } from "@/types/openapi"
import useSWR from "swr"
@@ -49,11 +49,12 @@ const MDXContentClient = dynamic<MDXContentClientProps>(
const TagSection = ({ tag }: TagSectionProps) => {
const { activePath, setActivePath } = useSidebar()
const router = useRouter()
const [loadPaths, setLoadPaths] = useState(false)
const slugTagName = useMemo(() => getSectionId([tag.name]), [tag])
const { area } = useArea()
const pathname = usePathname()
const { scrollableElement, scrollToElement } = useScrollController()
const { scrollableElement, scrollToTop } = useScrollController()
const { data } = useSWR<{
schema: SchemaObject
}>(
@@ -84,10 +85,14 @@ const TagSection = ({ tag }: TagSectionProps) => {
// ensure that the hash link doesn't change if it links to an inner path
const currentHashArr = location.hash.replace("#", "").split("_")
if (currentHashArr.length < 2 || currentHashArr[0] !== slugTagName) {
// can't use next router as it doesn't support
// changing url without scrolling
history.replaceState({}, "", `#${slugTagName}`)
setActivePath(slugTagName)
if (location.hash !== slugTagName) {
router.push(`#${slugTagName}`, {
scroll: false,
})
}
if (activePath !== slugTagName) {
setActivePath(slugTagName)
}
}
}
},
@@ -98,8 +103,11 @@ const TagSection = ({ tag }: TagSectionProps) => {
const tagName = activePath.split("_")
if (tagName.length === 1 && tagName[0] === slugTagName) {
const elm = document.getElementById(tagName[0])
if (elm && !checkElementInViewport(elm, 10)) {
scrollToElement(elm)
if (elm && !checkElementInViewport(elm, 0)) {
scrollToTop(
elm.offsetTop + (elm.offsetParent as HTMLElement)?.offsetTop,
0
)
}
} else if (tagName.length > 1 && tagName[0] === slugTagName) {
setLoadPaths(true)

View File

@@ -13,6 +13,7 @@ import { ExpandedDocument } from "@/types/openapi"
import getTagChildSidebarItems from "@/utils/get-tag-child-sidebar-items"
import { SidebarItem, SidebarItemSections } from "types"
import basePathUrl from "../../utils/base-path-url"
import { useRouter } from "next/navigation"
const TagSection = dynamic<TagSectionProps>(
async () => import("./Section")
@@ -31,8 +32,9 @@ const Tags = () => {
const [loadData, setLoadData] = useState<boolean>(false)
const [expand, setExpand] = useState<string>("")
const { baseSpecs, setBaseSpecs } = useBaseSpecs()
const { addItems, setActivePath } = useSidebar()
const { activePath, addItems, setActivePath } = useSidebar()
const { area, prevArea } = useArea()
const router = useRouter()
const { data } = useSWR<ExpandedDocument>(
loadData && !baseSpecs
@@ -89,8 +91,14 @@ const Tags = () => {
children: childItems,
loaded: childItems.length > 0,
onOpen: () => {
history.pushState({}, "", `#${tagPathName}`)
setActivePath(tagPathName)
if (location.hash !== tagPathName) {
router.push(`#${tagPathName}`, {
scroll: false,
})
}
if (activePath !== tagPathName) {
setActivePath(tagPathName)
}
},
})
})

View File

@@ -2,9 +2,6 @@
import {
AnalyticsProvider,
ColorModeProvider,
MobileProvider,
ModalProvider,
PageLoadingProvider,
ScrollControllerProvider,
SiteConfigProvider,
@@ -24,21 +21,15 @@ const Providers = ({ children }: ProvidersProps) => {
<AnalyticsProvider writeKey={process.env.NEXT_PUBLIC_SEGMENT_API_KEY}>
<SiteConfigProvider config={config}>
<PageLoadingProvider>
<ModalProvider>
<ColorModeProvider>
<BaseSpecsProvider>
<ScrollControllerProvider scrollableSelector="#main">
<SidebarProvider>
<MainNavProvider>
<SearchProvider>
<MobileProvider>{children}</MobileProvider>
</SearchProvider>
</MainNavProvider>
</SidebarProvider>
</ScrollControllerProvider>
</BaseSpecsProvider>
</ColorModeProvider>
</ModalProvider>
<BaseSpecsProvider>
<ScrollControllerProvider scrollableSelector="#main">
<SidebarProvider>
<MainNavProvider>
<SearchProvider>{children}</SearchProvider>
</MainNavProvider>
</SidebarProvider>
</ScrollControllerProvider>
</BaseSpecsProvider>
</PageLoadingProvider>
</SiteConfigProvider>
</AnalyticsProvider>

View File

@@ -16,7 +16,7 @@ type MainNavProviderProps = {
}
export const MainNavProvider = ({ children }: MainNavProviderProps) => {
const isBrowser = useIsBrowser()
const { isBrowser } = useIsBrowser()
const pathname = usePathname()
const navigationDropdownItems = useMemo(
() =>

View File

@@ -12,7 +12,7 @@ type PageTitleProviderProps = {
}
const PageTitleProvider = ({ children }: PageTitleProviderProps) => {
const { activePath, getActiveItem } = useSidebar()
const { activePath, activeItem } = useSidebar()
const { area } = useArea()
useEffect(() => {
@@ -21,7 +21,6 @@ const PageTitleProvider = ({ children }: PageTitleProviderProps) => {
if (!activePath?.length) {
document.title = titleSuffix
} else {
const activeItem = getActiveItem()
if (activeItem?.path === activePath) {
document.title = `${activeItem?.title} - ${titleSuffix}`
} else {
@@ -34,7 +33,7 @@ const PageTitleProvider = ({ children }: PageTitleProviderProps) => {
}
}
}
}, [activePath, area, getActiveItem])
}, [activePath, area, activeItem])
return (
<PageTitleContext.Provider value={null}>

View File

@@ -1,11 +1,10 @@
export default function checkElementInViewport(
element: Element,
percentage = 100,
height?: number
percentage = 100
) {
const rect = element.getBoundingClientRect()
const windowHeight: number | undefined =
height || window.innerHeight || document.documentElement.clientHeight
window.innerHeight || document.documentElement.clientHeight
return !(
Math.floor(100 - ((rect.top >= 0 ? 0 : rect.top) / +-rect.height) * 100) <

View File

@@ -15,7 +15,7 @@ type FeedbackProps = Omit<UiFeedbackProps, "event" | "pathName">
const Feedback = (props: FeedbackProps) => {
const pathname = usePathname()
const isBrowser = useIsBrowser()
const { isBrowser } = useIsBrowser()
const feedbackPathname = useMemo(() => basePathUrl(pathname), [pathname])
const reportLink = useMemo(

View File

@@ -2,11 +2,8 @@
import {
AnalyticsProvider,
ColorModeProvider,
HooksLoader,
LearningPathProvider,
MobileProvider,
ModalProvider,
NotificationProvider,
PaginationProvider,
ScrollControllerProvider,
@@ -25,34 +22,28 @@ const Providers = ({ children }: ProvidersProps) => {
return (
<AnalyticsProvider writeKey={process.env.NEXT_PUBLIC_SEGMENT_API_KEY}>
<SiteConfigProvider config={config}>
<MobileProvider>
<ColorModeProvider>
<ModalProvider>
<LearningPathProvider>
<NotificationProvider>
<ScrollControllerProvider scrollableSelector="#main">
<SidebarProvider>
<PaginationProvider>
<MainNavProvider>
<SearchProvider>
<HooksLoader
options={{
pageScrollManager: true,
currentLearningPath: true,
}}
>
{children}
</HooksLoader>
</SearchProvider>
</MainNavProvider>
</PaginationProvider>
</SidebarProvider>
</ScrollControllerProvider>
</NotificationProvider>
</LearningPathProvider>
</ModalProvider>
</ColorModeProvider>
</MobileProvider>
<LearningPathProvider>
<NotificationProvider>
<ScrollControllerProvider scrollableSelector="#main">
<SidebarProvider>
<PaginationProvider>
<MainNavProvider>
<SearchProvider>
<HooksLoader
options={{
pageScrollManager: true,
currentLearningPath: true,
}}
>
{children}
</HooksLoader>
</SearchProvider>
</MainNavProvider>
</PaginationProvider>
</SidebarProvider>
</ScrollControllerProvider>
</NotificationProvider>
</LearningPathProvider>
</SiteConfigProvider>
</AnalyticsProvider>
)

View File

@@ -17,7 +17,7 @@ type MainNavProviderProps = {
}
export const MainNavProvider = ({ children }: MainNavProviderProps) => {
const isBrowser = useIsBrowser()
const { isBrowser } = useIsBrowser()
const pathname = usePathname()
const navigationDropdownItems = useMemo(
() =>

View File

@@ -15,7 +15,7 @@ type FeedbackProps = Omit<UiFeedbackProps, "event" | "pathName">
export const Feedback = (props: FeedbackProps) => {
const pathname = usePathname()
const isBrowser = useIsBrowser()
const { isBrowser } = useIsBrowser()
const feedbackPathname = useMemo(() => basePathUrl(pathname), [pathname])
const reportLink = useMemo(

View File

@@ -2,11 +2,8 @@
import {
AnalyticsProvider,
ColorModeProvider,
HooksLoader,
LearningPathProvider,
MobileProvider,
ModalProvider,
NotificationProvider,
PaginationProvider,
ScrollControllerProvider,
@@ -25,36 +22,30 @@ const Providers = ({ children }: ProvidersProps) => {
return (
<AnalyticsProvider writeKey={process.env.NEXT_PUBLIC_SEGMENT_API_KEY}>
<SiteConfigProvider config={config}>
<MobileProvider>
<ColorModeProvider>
<ModalProvider>
<LearningPathProvider
baseUrl={process.env.NEXT_PUBLIC_BASE_PATH || "/resources"}
>
<NotificationProvider>
<ScrollControllerProvider scrollableSelector="#main">
<SidebarProvider>
<PaginationProvider>
<MainNavProvider>
<SearchProvider>
<HooksLoader
options={{
pageScrollManager: true,
currentLearningPath: true,
}}
>
{children}
</HooksLoader>
</SearchProvider>
</MainNavProvider>
</PaginationProvider>
</SidebarProvider>
</ScrollControllerProvider>
</NotificationProvider>
</LearningPathProvider>
</ModalProvider>
</ColorModeProvider>
</MobileProvider>
<LearningPathProvider
baseUrl={process.env.NEXT_PUBLIC_BASE_PATH || "/resources"}
>
<NotificationProvider>
<ScrollControllerProvider scrollableSelector="#main">
<SidebarProvider>
<PaginationProvider>
<MainNavProvider>
<SearchProvider>
<HooksLoader
options={{
pageScrollManager: true,
currentLearningPath: true,
}}
>
{children}
</HooksLoader>
</SearchProvider>
</MainNavProvider>
</PaginationProvider>
</SidebarProvider>
</ScrollControllerProvider>
</NotificationProvider>
</LearningPathProvider>
</SiteConfigProvider>
</AnalyticsProvider>
)

View File

@@ -17,7 +17,7 @@ type MainNavProviderProps = {
}
export const MainNavProvider = ({ children }: MainNavProviderProps) => {
const isBrowser = useIsBrowser()
const { isBrowser } = useIsBrowser()
const pathname = usePathname()
const navigationDropdownItems = useMemo(
() =>

View File

@@ -1,9 +1,6 @@
"use client"
import {
ColorModeProvider,
MobileProvider,
ModalProvider,
AnalyticsProvider,
ScrollControllerProvider,
SiteConfigProvider,
@@ -21,19 +18,13 @@ const Providers = ({ children }: ProvidersProps) => {
return (
<AnalyticsProvider writeKey={process.env.NEXT_PUBLIC_SEGMENT_API_KEY}>
<SiteConfigProvider config={siteConfig}>
<MobileProvider>
<ColorModeProvider>
<ModalProvider>
<ScrollControllerProvider scrollableSelector="#main">
<SidebarProvider>
<MainNavProvider>
<SearchProvider>{children}</SearchProvider>
</MainNavProvider>
</SidebarProvider>
</ScrollControllerProvider>
</ModalProvider>
</ColorModeProvider>
</MobileProvider>
<ScrollControllerProvider scrollableSelector="#main">
<SidebarProvider>
<MainNavProvider>
<SearchProvider>{children}</SearchProvider>
</MainNavProvider>
</SidebarProvider>
</ScrollControllerProvider>
</SiteConfigProvider>
</AnalyticsProvider>
)

View File

@@ -15,7 +15,7 @@ type MainNavProviderProps = {
}
export const MainNavProvider = ({ children }: MainNavProviderProps) => {
const isBrowser = useIsBrowser()
const { isBrowser } = useIsBrowser()
const navigationDropdownItems = useMemo(
() =>
getNavDropdownItems({

View File

@@ -15,7 +15,7 @@ type FeedbackProps = Omit<UiFeedbackProps, "event" | "pathName">
const Feedback = (props: FeedbackProps) => {
const pathname = usePathname()
const isBrowser = useIsBrowser()
const { isBrowser } = useIsBrowser()
const feedbackPathname = useMemo(() => basePathUrl(pathname), [pathname])
const reportLink = useMemo(

View File

@@ -15,7 +15,7 @@ type MainNavProviderProps = {
}
export const MainNavProvider = ({ children }: MainNavProviderProps) => {
const isBrowser = useIsBrowser()
const { isBrowser } = useIsBrowser()
const navigationDropdownItems = useMemo(
() =>
getNavDropdownItems({

View File

@@ -13,7 +13,7 @@ export type Bannerv2Props = {
export const Bannerv2 = ({ className }: Bannerv2Props) => {
const [show, setShow] = useState(false)
const isBrowser = useIsBrowser()
const { isBrowser } = useIsBrowser()
useEffect(() => {
if (!isBrowser) {

View File

@@ -22,7 +22,7 @@ export const ChildDocs = ({
hideTitle = false,
childLevel = 1,
}: ChildDocsProps) => {
const { currentItems, getActiveItem } = useSidebar()
const { currentItems, activeItem } = useSidebar()
const filterType = useMemo(() => {
return showItems !== undefined
? "show"
@@ -75,7 +75,7 @@ export const ChildDocs = ({
? Object.assign({}, currentItems)
: undefined
: {
default: [...(getActiveItem()?.children || [])],
default: [...(activeItem?.children || [])],
}
if (filterType === "all" || !targetItems) {
return targetItems
@@ -85,7 +85,7 @@ export const ChildDocs = ({
...targetItems,
default: filterItems(targetItems.default),
}
}, [currentItems, type, getActiveItem, filterItems])
}, [currentItems, type, activeItem, filterItems])
const filterNonInteractiveItems = (
items: SidebarItem[] | undefined

View File

@@ -7,7 +7,7 @@ import Link from "next/link"
import { SidebarItemLink } from "types"
export const MainNavBreadcrumbs = () => {
const { currentItems, getActiveItem } = useSidebar()
const { currentItems, activeItem } = useSidebar()
const {
activeItem: mainNavActiveItem,
breadcrumbOptions: { showCategories },
@@ -63,7 +63,6 @@ export const MainNavBreadcrumbs = () => {
)
}
const activeItem = getActiveItem()
if (activeItem && !mainNavActiveItem?.path.endsWith(activeItem.path)) {
if (
activeItem.parentItem &&
@@ -83,7 +82,7 @@ export const MainNavBreadcrumbs = () => {
}
return tempBreadcrumbItems
}, [currentItems, getActiveItem])
}, [currentItems, activeItem])
return (
<div

View File

@@ -0,0 +1,25 @@
"use client"
import React from "react"
import {
BrowserProvider,
ColorModeProvider,
MobileProvider,
ModalProvider,
} from "../../providers"
type RootProvidersProps = {
children: React.ReactNode
}
export const RootProviders = ({ children }: RootProvidersProps) => {
return (
<BrowserProvider>
<MobileProvider>
<ColorModeProvider>
<ModalProvider>{children}</ModalProvider>
</ColorModeProvider>
</MobileProvider>
</BrowserProvider>
)
}

View File

@@ -76,6 +76,12 @@ export const SidebarItemLink = ({
newTopCalculator,
])
useEffect(() => {
if (active && isMobile) {
setSidebarOpen(false)
}
}, [active, isMobile])
const hasChildren = useMemo(() => {
return !item.isChildSidebar && (item.children?.length || 0) > 0
}, [item.children])
@@ -108,14 +114,6 @@ export const SidebarItemLink = ({
"flex justify-between items-center gap-[6px]",
className
)}
scroll={true}
onClick={() => {
if (isMobile) {
setSidebarOpen(false)
}
}}
replace={!item.isPathHref}
shallow={!item.isPathHref}
{...item.linkProps}
>
<span

View File

@@ -16,7 +16,7 @@ import { TocMenu } from "./Menu"
export const Toc = () => {
const [items, setItems] = useState<ToCItemUi[]>([])
const [showMenu, setShowMenu] = useState(false)
const isBrowser = useIsBrowser()
const { isBrowser } = useIsBrowser()
const { items: headingItems, activeItemId } = useActiveOnScroll({})
const [maxHeight, setMaxHeight] = useState(0)
const { scrollableElement } = useScrollController()

View File

@@ -42,7 +42,7 @@ const TypeListItem = ({
sectionTitle,
referenceType = "method",
}: TypeListItemProps) => {
const isBrowser = useIsBrowser()
const { isBrowser } = useIsBrowser()
const pathname = usePathname()
const {
config: { baseUrl, basePath },

View File

@@ -53,6 +53,7 @@ export * from "./Notification/Item/Layout/Default"
export * from "./Pagination"
export * from "./Prerequisites"
export * from "./Rating"
export * from "./RootProviders"
export * from "./Search"
export * from "./Search/EmptyQueryBoundary"
export * from "./Search/Hits"

View File

@@ -5,7 +5,6 @@ export * from "./use-collapsible-code-lines"
export * from "./use-copy"
export * from "./use-current-learning-path"
export * from "./use-is-external-link"
export * from "./use-is-browser"
export * from "./use-keyboard-shortcut"
export * from "./use-page-scroll-manager"
export * from "./use-request-runner"

View File

@@ -24,7 +24,7 @@ export const useActiveOnScroll = ({
const [items, setItems] = useState<ActiveOnScrollItem[]>([])
const [activeItemId, setActiveItemId] = useState("")
const { scrollableElement } = useScrollController()
const isBrowser = useIsBrowser()
const { isBrowser } = useIsBrowser()
const pathname = usePathname()
const root = useMemo(() => {
if (!enable) {
@@ -86,6 +86,8 @@ export const useActiveOnScroll = ({
return
}
const headings = getHeadingsInElm()
let selectedHeadingByHash: HTMLHeadingElement | undefined = undefined
const hash = location.hash.replace("#", "")
let closestPositiveHeading: HTMLHeadingElement | undefined = undefined
let closestNegativeHeading: HTMLHeadingElement | undefined = undefined
let closestPositiveDistance = Infinity
@@ -97,6 +99,9 @@ export const useActiveOnScroll = ({
: 0
headings?.forEach((heading) => {
if (heading.id === hash) {
selectedHeadingByHash = heading as HTMLHeadingElement
}
const headingDistance = heading.getBoundingClientRect().top
if (headingDistance > 0 && headingDistance < closestPositiveDistance) {
@@ -126,6 +131,8 @@ export const useActiveOnScroll = ({
setActiveItemId(
chosenClosest
? (chosenClosest as HTMLHeadingElement).id
: selectedHeadingByHash
? (selectedHeadingByHash as HTMLHeadingElement).id
: items.length
? useDefaultIfNoActive
? items[0].heading.id

View File

@@ -12,7 +12,7 @@ export const useClickOutside = ({
elmRef,
onClickOutside,
}: UseClickOutsideProps) => {
const isBrowser = useIsBrowser()
const { isBrowser } = useIsBrowser()
const checkClickOutside = useCallback(
(e: MouseEvent) => {

View File

@@ -1,13 +0,0 @@
"use client"
import { useEffect, useState } from "react"
export const useIsBrowser = () => {
const [isBrowser, setIsBrowser] = useState(false)
useEffect(() => {
setIsBrowser(typeof window !== "undefined")
}, [])
return isBrowser
}

View File

@@ -16,7 +16,7 @@ export const MainContentLayout = ({
mainWrapperClasses,
showBanner = true,
}: MainContentLayoutProps) => {
const isBrowser = useIsBrowser()
const { isBrowser } = useIsBrowser()
const { desktopSidebarOpen } = useSidebar()
useEffect(() => {

View File

@@ -1,6 +1,6 @@
import React from "react"
import clsx from "clsx"
import { Sidebar, SidebarProps } from "@/components"
import { RootProviders, Sidebar, SidebarProps } from "@/components"
import { MobileNavigation } from "../components/MobileNavigation"
import { Toc } from "../components/Toc"
import { MainContentLayout, MainContentLayoutProps } from "./main-content"
@@ -36,14 +36,16 @@ export const RootLayout = ({
bodyClassName
)}
>
<ProvidersComponent>
<MobileNavigation />
<Sidebar {...sidebarProps} />
<div className={clsx("relative", "h-screen", "flex")}>
<MainContentLayout {...mainProps} />
{showToc && <Toc />}
</div>
</ProvidersComponent>
<RootProviders>
<ProvidersComponent>
<MobileNavigation />
<Sidebar {...sidebarProps} />
<div className={clsx("relative", "h-screen", "flex")}>
<MainContentLayout {...mainProps} />
{showToc && <Toc />}
</div>
</ProvidersComponent>
</RootProviders>
</body>
</html>
)

View File

@@ -0,0 +1,41 @@
"use client"
import React, { useContext, useEffect, useState } from "react"
type BrowserContextType = {
isBrowser: boolean
}
const BrowserContext = React.createContext<BrowserContextType | null>(null)
type BrowserProviderProps = {
children: React.ReactNode
}
export const BrowserProvider = ({ children }: BrowserProviderProps) => {
const [isBrowser, setIsBrowser] = useState(false)
useEffect(() => {
setIsBrowser(typeof window !== "undefined")
}, [])
return (
<BrowserContext.Provider
value={{
isBrowser,
}}
>
{children}
</BrowserContext.Provider>
)
}
export const useIsBrowser = () => {
const context = useContext(BrowserContext)
if (!context) {
throw new Error("useIsBrowser must be used within a BrowserProvider")
}
return context
}

View File

@@ -1,10 +1,9 @@
"use client"
import { useIsBrowser } from "@/hooks"
import { getLearningPath } from "@/utils/learning-paths"
import React, { createContext, useContext, useEffect, useState } from "react"
import { LearningPathFinishType } from "@/components/LearningPath/Finish"
import { useAnalytics } from "docs-ui"
import { useAnalytics, useIsBrowser } from "docs-ui"
import { usePathname, useRouter } from "next/navigation"
export type LearningPathType = {
@@ -54,7 +53,7 @@ export const LearningPathProvider: React.FC<LearningPathProviderProps> = ({
}) => {
const [path, setPath] = useState<LearningPathType | null>(null)
const [currentStep, setCurrentStep] = useState(-1)
const isBrowser = useIsBrowser()
const { isBrowser } = useIsBrowser()
const pathname = usePathname()
const router = useRouter()
const { track } = useAnalytics()

View File

@@ -6,13 +6,13 @@ import React, {
useCallback,
useContext,
useEffect,
useMemo,
useReducer,
useRef,
useState,
} from "react"
import { usePathname, useRouter } from "next/navigation"
import { getScrolledTop } from "@/utils"
import { useIsBrowser } from "@/hooks"
import {
SidebarItemSections,
SidebarItem,
@@ -22,6 +22,7 @@ import {
SidebarItemCategory,
SidebarItemLinkWithParent,
} from "types"
import { useIsBrowser } from "../BrowserProvider"
export type CurrentItemsState = SidebarSectionItems & {
previousSidebar?: CurrentItemsState
@@ -35,7 +36,7 @@ export type SidebarContextType = {
items: SidebarSectionItems
currentItems: CurrentItemsState | undefined
activePath: string | null
getActiveItem: () => SidebarItemLinkWithParent | undefined
activeItem: SidebarItemLinkWithParent | undefined
setActivePath: (path: string | null) => void
isLinkActive: (item: SidebarItem, checkChildren?: boolean) => boolean
isChildrenActive: (item: SidebarItemCategory) => boolean
@@ -86,6 +87,8 @@ export type ActionType =
replacementItems: SidebarSectionItems
}
type LinksMap = Map<string, SidebarItemLinkWithParent>
const areItemsEqual = (itemA: SidebarItem, itemB: SidebarItem): boolean => {
if (itemA.type === "separator" || itemB.type === "separator") {
return false
@@ -122,6 +125,32 @@ const findItem = (
return foundItem
}
const getLinksMap = (
items: SidebarItem[],
initMap?: LinksMap,
parentItem?: InteractiveSidebarItem
): LinksMap => {
const map: LinksMap = initMap || new Map()
items.forEach((item) => {
if (item.type === "separator") {
return
}
if (item.type === "link") {
map.set(item.path, {
...item,
parentItem,
})
}
if (item.children?.length) {
getLinksMap(item.children, map, item)
}
})
return map
}
export const reducer = (state: SidebarSectionItems, actionData: ActionType) => {
if (actionData.type === "replace") {
return actionData.replacementItems
@@ -226,6 +255,20 @@ export const SidebarProvider = ({
CurrentItemsState | undefined
>()
const [activePath, setActivePath] = useState<string | null>("")
const linksMap: LinksMap = useMemo(() => {
return new Map([
...getLinksMap(items.mobile),
...getLinksMap(items.default),
])
}, [items])
const findItemInSection = useCallback(findItem, [])
const activeItem = useMemo(() => {
if (activePath === null) {
return undefined
}
return linksMap.get(activePath)
}, [activePath, linksMap])
const [mobileSidebarOpen, setMobileSidebarOpen] = useState<boolean>(false)
const [sidebarTopHeight, setSidebarTopHeight] = useState(0)
const [desktopSidebarOpen, setDesktopSidebarOpen] = useState(true)
@@ -233,35 +276,20 @@ export const SidebarProvider = ({
const pathname = usePathname()
const router = useRouter()
const isBrowser = useIsBrowser()
const { isBrowser } = useIsBrowser()
const getResolvedScrollableElement = useCallback(() => {
return scrollableElement || window
}, [scrollableElement])
const findItemInSection = useCallback(findItem, [])
const isItemLoaded = useCallback(
(path: string) => {
const item =
findItemInSection(items.mobile, { path, type: "link" }) ||
findItemInSection(items.default, { path, type: "link" })
const item = linksMap.get(path)
return item?.loaded || false
},
[items]
[items, linksMap]
)
const getActiveItem = useCallback(() => {
if (activePath === null) {
return undefined
}
return (
findItemInSection(items.mobile, { path: activePath, type: "link" }) ||
findItemInSection(items.default, { path: activePath, type: "link" })
)
}, [activePath, items, findItemInSection])
const addItems = (newItems: SidebarItem[], options?: ActionOptionsType) => {
dispatch({
type: options?.parent ? "update" : "add",
@@ -322,10 +350,12 @@ export const SidebarProvider = ({
if (!currentSidebar && item.children?.length) {
const childSidebar =
getCurrentSidebar(item.children) ||
findItem(item.children, {
path: activePath || undefined,
type: "link",
})
(activePath
? findItem(item.children, {
path: activePath || undefined,
type: "link",
})
: undefined)
if (childSidebar) {
currentSidebar = childSidebar.isChildSidebar ? childSidebar : item
@@ -385,10 +415,16 @@ export const SidebarProvider = ({
const handleScroll = () => {
if (getScrolledTop(resolvedScrollableElement) === 0) {
setActivePath("")
// can't use next router as it doesn't support
// changing url without scrolling
history.replaceState({}, "", location.pathname)
const firstItemPath =
items.default.length && items.default[0].type === "link"
? items.default[0].path
: ""
setActivePath(firstItemPath)
if (firstItemPath) {
router.push(`#${firstItemPath}`, {
scroll: false,
})
}
}
}
@@ -540,7 +576,7 @@ export const SidebarProvider = ({
setMobileSidebarOpen,
desktopSidebarOpen,
setDesktopSidebarOpen,
getActiveItem,
activeItem,
staticSidebarItems,
disableActiveTransition,
shouldHandleHashChange,

View File

@@ -1,5 +1,6 @@
export * from "./AiAssistant"
export * from "./Analytics"
export * from "./BrowserProvider"
export * from "./ColorMode"
export * from "./LearningPath"
export * from "./MainNav"

View File

@@ -6304,17 +6304,10 @@ __metadata:
languageName: node
linkType: hard
"caniuse-lite@npm:^1.0.30001464, caniuse-lite@npm:^1.0.30001578, caniuse-lite@npm:^1.0.30001587":
version: 1.0.30001589
resolution: "caniuse-lite@npm:1.0.30001589"
checksum: 20debfb949413f603011bc7dacaf050010778bc4f8632c86fafd1bd0c43180c95ae7c31f6c82348f6309e5e221934e327c3607a216e3f09640284acf78cd6d4d
languageName: node
linkType: hard
"caniuse-lite@npm:^1.0.30001579":
version: 1.0.30001597
resolution: "caniuse-lite@npm:1.0.30001597"
checksum: 32dc315ffafacc8167286c95b05f41b3ce2818314ea913ffed6ceb7b58c64c38365ec250114d1ecceac34f1c77e5af089479e54b160c4a89b88fd25a98851b78
"caniuse-lite@npm:^1.0.30001464, caniuse-lite@npm:^1.0.30001578, caniuse-lite@npm:^1.0.30001579, caniuse-lite@npm:^1.0.30001587":
version: 1.0.30001666
resolution: "caniuse-lite@npm:1.0.30001666"
checksum: 2d49e9be676233c24717f12aad3d01b3e5f902b457fe1deefaa8d82e64786788a8f79381ae437c61b50e15c9aea8aeb59871b1d54cb4c28b9190d53d292e2339
languageName: node
linkType: hard