docs: fixed sidebar in API reference (#5871)

This commit is contained in:
Shahed Nasser authored and GitHub committed 2023-12-14 13:26:24 +02:00
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)
+6 -6
View File
@@ -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>
+7 -1
View File
@@ -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: [
{ {
+7 -9
View File
@@ -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>
+6 -1
View File
@@ -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
}
+2
View File
@@ -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
}