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:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
25
www/packages/docs-ui/src/components/RootProviders/index.tsx
Normal file
25
www/packages/docs-ui/src/components/RootProviders/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -42,7 +42,7 @@ const TypeListItem = ({
|
||||
sectionTitle,
|
||||
referenceType = "method",
|
||||
}: TypeListItemProps) => {
|
||||
const isBrowser = useIsBrowser()
|
||||
const { isBrowser } = useIsBrowser()
|
||||
const pathname = usePathname()
|
||||
const {
|
||||
config: { baseUrl, basePath },
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,7 +12,7 @@ export const useClickOutside = ({
|
||||
elmRef,
|
||||
onClickOutside,
|
||||
}: UseClickOutsideProps) => {
|
||||
const isBrowser = useIsBrowser()
|
||||
const { isBrowser } = useIsBrowser()
|
||||
|
||||
const checkClickOutside = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export const MainContentLayout = ({
|
||||
mainWrapperClasses,
|
||||
showBanner = true,
|
||||
}: MainContentLayoutProps) => {
|
||||
const isBrowser = useIsBrowser()
|
||||
const { isBrowser } = useIsBrowser()
|
||||
const { desktopSidebarOpen } = useSidebar()
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
41
www/packages/docs-ui/src/providers/BrowserProvider/index.tsx
Normal file
41
www/packages/docs-ui/src/providers/BrowserProvider/index.tsx
Normal 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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./AiAssistant"
|
||||
export * from "./Analytics"
|
||||
export * from "./BrowserProvider"
|
||||
export * from "./ColorMode"
|
||||
export * from "./LearningPath"
|
||||
export * from "./MainNav"
|
||||
|
||||
Reference in New Issue
Block a user