docs: fixed sidebar in API reference (#5871)
This commit is contained in:
1 parent
b5748ab59e
commit
c0ce969cd7
13 files changed
+109
-42
No files matched your search
@@ -1,9 +1,9 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { InView } from "react-intersection-observer"
|
import { InView } from "react-intersection-observer"
|
||||||
import { useSidebar } from "docs-ui"
|
import { isElmWindow, useScrollController, useSidebar } from "docs-ui"
|
||||||
import checkElementInViewport from "../../../utils/check-element-in-viewport"
|
import checkElementInViewport from "../../../utils/check-element-in-viewport"
|
||||||
import { useEffect } from "react"
|
import { useEffect, useMemo } from "react"
|
||||||
import getSectionId from "../../../utils/get-section-id"
|
import getSectionId from "../../../utils/get-section-id"
|
||||||
|
|
||||||
type H2Props = {
|
type H2Props = {
|
||||||
@@ -12,6 +12,7 @@ type H2Props = {
|
|||||||
|
|
||||||
const H2 = ({ addToSidebar = true, children, ...props }: H2Props) => {
|
const H2 = ({ addToSidebar = true, children, ...props }: H2Props) => {
|
||||||
const { activePath, setActivePath, addItems } = useSidebar()
|
const { activePath, setActivePath, addItems } = useSidebar()
|
||||||
|
const { getScrolledTop, scrollableElement } = useScrollController()
|
||||||
|
|
||||||
const handleViewChange = (
|
const handleViewChange = (
|
||||||
inView: boolean,
|
inView: boolean,
|
||||||
@@ -24,7 +25,7 @@ const H2 = ({ addToSidebar = true, children, ...props }: H2Props) => {
|
|||||||
if (
|
if (
|
||||||
(inView ||
|
(inView ||
|
||||||
checkElementInViewport(heading.parentElement || heading, 40)) &&
|
checkElementInViewport(heading.parentElement || heading, 40)) &&
|
||||||
window.scrollY !== 0 &&
|
getScrolledTop() !== 0 &&
|
||||||
activePath !== heading.id
|
activePath !== heading.id
|
||||||
) {
|
) {
|
||||||
// can't use next router as it doesn't support
|
// can't use next router as it doesn't support
|
||||||
@@ -50,6 +51,10 @@ const H2 = ({ addToSidebar = true, children, ...props }: H2Props) => {
|
|||||||
])
|
])
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const root = useMemo(() => {
|
||||||
|
return isElmWindow(scrollableElement) ? document.body : scrollableElement
|
||||||
|
}, [scrollableElement])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InView
|
<InView
|
||||||
as="h2"
|
as="h2"
|
||||||
@@ -59,6 +64,7 @@ const H2 = ({ addToSidebar = true, children, ...props }: H2Props) => {
|
|||||||
{...props}
|
{...props}
|
||||||
onChange={handleViewChange}
|
onChange={handleViewChange}
|
||||||
id={id}
|
id={id}
|
||||||
|
root={root}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</InView>
|
</InView>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import getSectionId from "@/utils/get-section-id"
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import dynamic from "next/dynamic"
|
import dynamic from "next/dynamic"
|
||||||
import { useInView } from "react-intersection-observer"
|
import { useInView } from "react-intersection-observer"
|
||||||
import { useSidebar } from "docs-ui"
|
import { isElmWindow, useScrollController, useSidebar } from "docs-ui"
|
||||||
import type { TagOperationCodeSectionProps } from "./CodeSection"
|
import type { TagOperationCodeSectionProps } from "./CodeSection"
|
||||||
import TagsOperationDescriptionSection from "./DescriptionSection"
|
import TagsOperationDescriptionSection from "./DescriptionSection"
|
||||||
import DividedLayout from "@/layouts/Divided"
|
import DividedLayout from "@/layouts/Divided"
|
||||||
@@ -40,10 +40,14 @@ const TagOperation = ({
|
|||||||
)
|
)
|
||||||
const nodeRef = useRef<Element | null>(null)
|
const nodeRef = useRef<Element | null>(null)
|
||||||
const { loading, removeLoading } = useLoading()
|
const { loading, removeLoading } = useLoading()
|
||||||
|
const { scrollableElement } = useScrollController()
|
||||||
|
const root = useMemo(() => {
|
||||||
|
return isElmWindow(scrollableElement) ? document.body : scrollableElement
|
||||||
|
}, [scrollableElement])
|
||||||
const { ref } = useInView({
|
const { ref } = useInView({
|
||||||
threshold: 0.3,
|
threshold: 0.3,
|
||||||
rootMargin: `112px 0px 112px 0px`,
|
rootMargin: `112px 0px 112px 0px`,
|
||||||
root: document.getElementById("main") || document.body,
|
root,
|
||||||
onChange: (changedInView) => {
|
onChange: (changedInView) => {
|
||||||
if (changedInView) {
|
if (changedInView) {
|
||||||
if (!show) {
|
if (!show) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import getSectionId from "@/utils/get-section-id"
|
|||||||
import type { OpenAPIV3 } from "openapi-types"
|
import type { OpenAPIV3 } from "openapi-types"
|
||||||
import { useInView } from "react-intersection-observer"
|
import { useInView } from "react-intersection-observer"
|
||||||
import { useEffect, useMemo, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
import { useSidebar } from "docs-ui"
|
import { isElmWindow, useScrollController, useSidebar } from "docs-ui"
|
||||||
import dynamic from "next/dynamic"
|
import dynamic from "next/dynamic"
|
||||||
import type { SectionProps } from "../../Section"
|
import type { SectionProps } from "../../Section"
|
||||||
import type { MDXContentClientProps } from "../../MDXContent/Client"
|
import type { MDXContentClientProps } from "../../MDXContent/Client"
|
||||||
@@ -40,10 +40,14 @@ const TagSection = ({ tag }: TagSectionProps) => {
|
|||||||
const slugTagName = useMemo(() => getSectionId([tag.name]), [tag])
|
const slugTagName = useMemo(() => getSectionId([tag.name]), [tag])
|
||||||
const { area } = useArea()
|
const { area } = useArea()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
const { scrollableElement } = useScrollController()
|
||||||
|
const root = useMemo(() => {
|
||||||
|
return isElmWindow(scrollableElement) ? document.body : scrollableElement
|
||||||
|
}, [scrollableElement])
|
||||||
const { ref } = useInView({
|
const { ref } = useInView({
|
||||||
threshold: 0.5,
|
threshold: 0.5,
|
||||||
rootMargin: `112px 0px 112px 0px`,
|
rootMargin: `112px 0px 112px 0px`,
|
||||||
root: document.getElementById("main") || document.body,
|
root,
|
||||||
onChange: (inView) => {
|
onChange: (inView) => {
|
||||||
if (inView && !loadPaths) {
|
if (inView && !loadPaths) {
|
||||||
setLoadPaths(true)
|
setLoadPaths(true)
|
||||||
|
|||||||
@@ -25,15 +25,15 @@ const Providers = ({ children }: ProvidersProps) => {
|
|||||||
<ModalProvider>
|
<ModalProvider>
|
||||||
<ColorModeProvider>
|
<ColorModeProvider>
|
||||||
<BaseSpecsProvider>
|
<BaseSpecsProvider>
|
||||||
<SidebarProvider>
|
<ScrollControllerProvider scrollableSelector="#main">
|
||||||
<NavbarProvider>
|
<SidebarProvider>
|
||||||
<ScrollControllerProvider scrollableSelector="#main">
|
<NavbarProvider>
|
||||||
<SearchProvider>
|
<SearchProvider>
|
||||||
<MobileProvider>{children}</MobileProvider>
|
<MobileProvider>{children}</MobileProvider>
|
||||||
</SearchProvider>
|
</SearchProvider>
|
||||||
</ScrollControllerProvider>
|
</NavbarProvider>
|
||||||
</NavbarProvider>
|
</SidebarProvider>
|
||||||
</SidebarProvider>
|
</ScrollControllerProvider>
|
||||||
</BaseSpecsProvider>
|
</BaseSpecsProvider>
|
||||||
</ColorModeProvider>
|
</ColorModeProvider>
|
||||||
</ModalProvider>
|
</ModalProvider>
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { SidebarProvider as UiSidebarProvider, usePageLoading } from "docs-ui"
|
import {
|
||||||
|
SidebarProvider as UiSidebarProvider,
|
||||||
|
usePageLoading,
|
||||||
|
useScrollController,
|
||||||
|
} from "docs-ui"
|
||||||
|
|
||||||
type SidebarProviderProps = {
|
type SidebarProviderProps = {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
@@ -7,12 +11,14 @@ type SidebarProviderProps = {
|
|||||||
|
|
||||||
const SidebarProvider = ({ children }: SidebarProviderProps) => {
|
const SidebarProvider = ({ children }: SidebarProviderProps) => {
|
||||||
const { isLoading, setIsLoading } = usePageLoading()
|
const { isLoading, setIsLoading } = usePageLoading()
|
||||||
|
const { scrollableElement } = useScrollController()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UiSidebarProvider
|
<UiSidebarProvider
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
setIsLoading={setIsLoading}
|
setIsLoading={setIsLoading}
|
||||||
shouldHandleHashChange={true}
|
shouldHandleHashChange={true}
|
||||||
|
scrollableElement={scrollableElement}
|
||||||
initialItems={{
|
initialItems={{
|
||||||
top: [
|
top: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,15 +22,13 @@ const Providers = ({ children }: ProvidersProps) => {
|
|||||||
<MobileProvider>
|
<MobileProvider>
|
||||||
<ColorModeProvider>
|
<ColorModeProvider>
|
||||||
<ModalProvider>
|
<ModalProvider>
|
||||||
<SidebarProvider>
|
<ScrollControllerProvider scrollableSelector="#main">
|
||||||
<NavbarProvider basePath={process.env.NEXT_PUBLIC_BASE_PATH}>
|
<SidebarProvider>
|
||||||
<SearchProvider>
|
<NavbarProvider basePath={process.env.NEXT_PUBLIC_BASE_PATH}>
|
||||||
<ScrollControllerProvider scrollableSelector="#main">
|
<SearchProvider>{children}</SearchProvider>
|
||||||
{children}
|
</NavbarProvider>
|
||||||
</ScrollControllerProvider>
|
</SidebarProvider>
|
||||||
</SearchProvider>
|
</ScrollControllerProvider>
|
||||||
</NavbarProvider>
|
|
||||||
</SidebarProvider>
|
|
||||||
</ModalProvider>
|
</ModalProvider>
|
||||||
</ColorModeProvider>
|
</ColorModeProvider>
|
||||||
</MobileProvider>
|
</MobileProvider>
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { SidebarProvider as UiSidebarProvider } from "docs-ui"
|
import {
|
||||||
|
SidebarProvider as UiSidebarProvider,
|
||||||
|
useScrollController,
|
||||||
|
} from "docs-ui"
|
||||||
import { docsConfig } from "@/config/docs"
|
import { docsConfig } from "@/config/docs"
|
||||||
|
|
||||||
type SidebarProviderProps = {
|
type SidebarProviderProps = {
|
||||||
@@ -6,10 +9,12 @@ type SidebarProviderProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SidebarProvider = ({ children }: SidebarProviderProps) => {
|
const SidebarProvider = ({ children }: SidebarProviderProps) => {
|
||||||
|
const { scrollableElement } = useScrollController()
|
||||||
return (
|
return (
|
||||||
<UiSidebarProvider
|
<UiSidebarProvider
|
||||||
initialItems={docsConfig.sidebar}
|
initialItems={docsConfig.sidebar}
|
||||||
shouldHandlePathChange={true}
|
shouldHandlePathChange={true}
|
||||||
|
scrollableElement={scrollableElement}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</UiSidebarProvider>
|
</UiSidebarProvider>
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ export const SidebarItem = ({
|
|||||||
}: SidebarItemProps) => {
|
}: SidebarItemProps) => {
|
||||||
const [showLoading, setShowLoading] = useState(false)
|
const [showLoading, setShowLoading] = useState(false)
|
||||||
const { isItemActive, setMobileSidebarOpen: setSidebarOpen } = useSidebar()
|
const { isItemActive, setMobileSidebarOpen: setSidebarOpen } = useSidebar()
|
||||||
const active = useMemo(() => {
|
const active = useMemo(
|
||||||
return isItemActive(item, nested)
|
() => isItemActive(item, nested),
|
||||||
}, [isItemActive, item, nested])
|
[isItemActive, item, nested]
|
||||||
|
)
|
||||||
const collapsed = !expandItems && !isItemActive(item, true)
|
const collapsed = !expandItems && !isItemActive(item, true)
|
||||||
const ref = useRef<HTMLLIElement>(null)
|
const ref = useRef<HTMLLIElement>(null)
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ import React, {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
|
useState,
|
||||||
} from "react"
|
} from "react"
|
||||||
|
import { getScrolledTop as getScrolledTopUtil } from "../../utils"
|
||||||
|
|
||||||
type EventFunc = (...args: never[]) => unknown
|
type EventFunc = (...args: never[]) => unknown
|
||||||
|
|
||||||
@@ -54,7 +56,9 @@ type ScrollController = {
|
|||||||
/** Disable scroll events in `useScrollPosition`. */
|
/** Disable scroll events in `useScrollPosition`. */
|
||||||
disableScrollEvents: () => void
|
disableScrollEvents: () => void
|
||||||
/** Retrieves the scrollable element. By default, it's window. */
|
/** Retrieves the scrollable element. By default, it's window. */
|
||||||
getScrollableElement: () => Element | Window
|
scrollableElement: Element | Window | undefined
|
||||||
|
/** Retrieves the scroll top if the scrollable element */
|
||||||
|
getScrolledTop: () => number
|
||||||
}
|
}
|
||||||
|
|
||||||
function useScrollControllerContextValue(
|
function useScrollControllerContextValue(
|
||||||
@@ -62,9 +66,19 @@ function useScrollControllerContextValue(
|
|||||||
): ScrollController {
|
): ScrollController {
|
||||||
const scrollEventsEnabledRef = useRef(true)
|
const scrollEventsEnabledRef = useRef(true)
|
||||||
|
|
||||||
const getScrollableElement = useCallback(() => {
|
const [scrollableElement, setScrollableElement] = useState<
|
||||||
return (document.querySelector(scrollableSelector) as Element) || window
|
Element | Window | undefined
|
||||||
}, [scrollableSelector])
|
>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setScrollableElement(
|
||||||
|
(document.querySelector(scrollableSelector) as Element) || window
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getScrolledTop = () => {
|
||||||
|
return scrollableElement ? getScrolledTopUtil(scrollableElement) : 0
|
||||||
|
}
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -75,9 +89,10 @@ function useScrollControllerContextValue(
|
|||||||
disableScrollEvents: () => {
|
disableScrollEvents: () => {
|
||||||
scrollEventsEnabledRef.current = false
|
scrollEventsEnabledRef.current = false
|
||||||
},
|
},
|
||||||
getScrollableElement,
|
scrollableElement,
|
||||||
|
getScrolledTop,
|
||||||
}),
|
}),
|
||||||
[getScrollableElement]
|
[scrollableElement]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +191,7 @@ type UseScrollPositionSaver = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function useScrollPositionSaver(): UseScrollPositionSaver {
|
function useScrollPositionSaver(): UseScrollPositionSaver {
|
||||||
const { getScrollableElement } = useScrollController()
|
const { scrollableElement } = useScrollController()
|
||||||
const lastElementRef = useRef<{ elem: HTMLElement | null; top: number }>({
|
const lastElementRef = useRef<{ elem: HTMLElement | null; top: number }>({
|
||||||
elem: null,
|
elem: null,
|
||||||
top: 0,
|
top: 0,
|
||||||
@@ -199,7 +214,7 @@ function useScrollPositionSaver(): UseScrollPositionSaver {
|
|||||||
const newTop = elem.getBoundingClientRect().top
|
const newTop = elem.getBoundingClientRect().top
|
||||||
const heightDiff = newTop - top
|
const heightDiff = newTop - top
|
||||||
if (heightDiff) {
|
if (heightDiff) {
|
||||||
getScrollableElement().scrollBy({ left: 0, top: heightDiff })
|
scrollableElement?.scrollBy({ left: 0, top: heightDiff })
|
||||||
}
|
}
|
||||||
lastElementRef.current = { elem: null, top: 0 }
|
lastElementRef.current = { elem: null, top: 0 }
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
} from "react"
|
} from "react"
|
||||||
import { usePathname } from "next/navigation"
|
import { usePathname } from "next/navigation"
|
||||||
|
import { getScrolledTop } from "../../utils"
|
||||||
|
|
||||||
export enum SidebarItemSections {
|
export enum SidebarItemSections {
|
||||||
TOP = "top",
|
TOP = "top",
|
||||||
@@ -129,6 +130,7 @@ export type SidebarProviderProps = {
|
|||||||
initialItems?: SidebarSectionItemsType
|
initialItems?: SidebarSectionItemsType
|
||||||
shouldHandleHashChange?: boolean
|
shouldHandleHashChange?: boolean
|
||||||
shouldHandlePathChange?: boolean
|
shouldHandlePathChange?: boolean
|
||||||
|
scrollableElement?: Element | Window
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SidebarProvider = ({
|
export const SidebarProvider = ({
|
||||||
@@ -138,6 +140,7 @@ export const SidebarProvider = ({
|
|||||||
initialItems,
|
initialItems,
|
||||||
shouldHandleHashChange = false,
|
shouldHandleHashChange = false,
|
||||||
shouldHandlePathChange = false,
|
shouldHandlePathChange = false,
|
||||||
|
scrollableElement,
|
||||||
}: SidebarProviderProps) => {
|
}: SidebarProviderProps) => {
|
||||||
const [items, dispatch] = useReducer(reducer, {
|
const [items, dispatch] = useReducer(reducer, {
|
||||||
top: initialItems?.top || [],
|
top: initialItems?.top || [],
|
||||||
@@ -148,6 +151,9 @@ export const SidebarProvider = ({
|
|||||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState<boolean>(false)
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState<boolean>(false)
|
||||||
const [desktopSidebarOpen, setDesktopSidebarOpen] = useState(true)
|
const [desktopSidebarOpen, setDesktopSidebarOpen] = useState(true)
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
const getResolvedScrollableElement = useCallback(() => {
|
||||||
|
return scrollableElement || window
|
||||||
|
}, [scrollableElement])
|
||||||
|
|
||||||
const findItemInSection = useCallback(
|
const findItemInSection = useCallback(
|
||||||
(
|
(
|
||||||
@@ -249,15 +255,21 @@ export const SidebarProvider = ({
|
|||||||
}
|
}
|
||||||
}, [activePath])
|
}, [activePath])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldHandleHashChange) {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
}, [shouldHandleHashChange])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shouldHandleHashChange) {
|
if (!shouldHandleHashChange) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
init()
|
const resolvedScrollableElement = getResolvedScrollableElement()
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (window.scrollY === 0) {
|
if (getScrolledTop(resolvedScrollableElement) === 0) {
|
||||||
setActivePath("")
|
setActivePath("")
|
||||||
// can't use next router as it doesn't support
|
// can't use next router as it doesn't support
|
||||||
// changing url without scrolling
|
// changing url without scrolling
|
||||||
@@ -265,14 +277,17 @@ export const SidebarProvider = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("scroll", handleScroll)
|
resolvedScrollableElement.addEventListener("scroll", handleScroll)
|
||||||
window.addEventListener("hashchange", handleHashChange)
|
resolvedScrollableElement.addEventListener("hashchange", handleHashChange)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("scroll", handleScroll)
|
resolvedScrollableElement.removeEventListener("scroll", handleScroll)
|
||||||
window.removeEventListener("hashchange", handleHashChange)
|
resolvedScrollableElement.removeEventListener(
|
||||||
|
"hashchange",
|
||||||
|
handleHashChange
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}, [handleHashChange, shouldHandleHashChange])
|
}, [handleHashChange, shouldHandleHashChange, getResolvedScrollableElement])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoading && items.top.length && items.bottom.length) {
|
if (isLoading && items.top.length && items.bottom.length) {
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { isElmWindow } from "./is-elm-window"
|
||||||
|
|
||||||
|
export function getScrolledTop(elm?: Element | Window): number {
|
||||||
|
if (!elm) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return isElmWindow(elm) ? elm.scrollY : elm.scrollTop
|
||||||
|
}
|
||||||
@@ -3,4 +3,6 @@ export * from "./capitalize"
|
|||||||
export * from "./check-sidebar-item-visibility"
|
export * from "./check-sidebar-item-visibility"
|
||||||
export * from "./dom-utils"
|
export * from "./dom-utils"
|
||||||
export * from "./format-report-link"
|
export * from "./format-report-link"
|
||||||
|
export * from "./get-scrolled-top"
|
||||||
|
export * from "./is-elm-window"
|
||||||
export * from "./swr-fetcher"
|
export * from "./swr-fetcher"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function isElmWindow(elm: unknown): elm is Window {
|
||||||
|
return typeof window !== "undefined" && elm === window
|
||||||
|
}
|
||||||
Reference in new issue
Block a user