From c0ce969cd7e2048263f1742d7d83627c027f4a7e Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Thu, 14 Dec 2023 13:26:24 +0200 Subject: [PATCH] docs: fixed sidebar in API reference (#5871) --- .../components/MDXComponents/H2/index.tsx | 12 +++++-- .../components/Tags/Operation/index.tsx | 8 +++-- .../components/Tags/Section/index.tsx | 8 +++-- www/apps/api-reference/providers/index.tsx | 12 +++---- www/apps/api-reference/providers/sidebar.tsx | 8 ++++- www/apps/ui/src/providers/index.tsx | 16 +++++----- www/apps/ui/src/providers/sidebar.tsx | 7 ++++- .../src/components/Sidebar/Item/index.tsx | 7 +++-- .../src/hooks/use-scroll-utils/index.tsx | 31 ++++++++++++++----- .../docs-ui/src/providers/Sidebar/index.tsx | 29 ++++++++++++----- .../docs-ui/src/utils/get-scrolled-top.ts | 8 +++++ www/packages/docs-ui/src/utils/index.ts | 2 ++ .../docs-ui/src/utils/is-elm-window.ts | 3 ++ 13 files changed, 109 insertions(+), 42 deletions(-) create mode 100644 www/packages/docs-ui/src/utils/get-scrolled-top.ts create mode 100644 www/packages/docs-ui/src/utils/is-elm-window.ts diff --git a/www/apps/api-reference/components/MDXComponents/H2/index.tsx b/www/apps/api-reference/components/MDXComponents/H2/index.tsx index 8430a09d6af0d..002814272d95e 100644 --- a/www/apps/api-reference/components/MDXComponents/H2/index.tsx +++ b/www/apps/api-reference/components/MDXComponents/H2/index.tsx @@ -1,9 +1,9 @@ "use client" 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 { useEffect } from "react" +import { useEffect, useMemo } from "react" import getSectionId from "../../../utils/get-section-id" type H2Props = { @@ -12,6 +12,7 @@ type H2Props = { const H2 = ({ addToSidebar = true, children, ...props }: H2Props) => { const { activePath, setActivePath, addItems } = useSidebar() + const { getScrolledTop, scrollableElement } = useScrollController() const handleViewChange = ( inView: boolean, @@ -24,7 +25,7 @@ const H2 = ({ addToSidebar = true, children, ...props }: H2Props) => { if ( (inView || checkElementInViewport(heading.parentElement || heading, 40)) && - window.scrollY !== 0 && + getScrolledTop() !== 0 && activePath !== heading.id ) { // 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 ( { {...props} onChange={handleViewChange} id={id} + root={root} > {children} diff --git a/www/apps/api-reference/components/Tags/Operation/index.tsx b/www/apps/api-reference/components/Tags/Operation/index.tsx index 3ad5075b3b8c3..3eeaeb2cb5c6a 100644 --- a/www/apps/api-reference/components/Tags/Operation/index.tsx +++ b/www/apps/api-reference/components/Tags/Operation/index.tsx @@ -7,7 +7,7 @@ import getSectionId from "@/utils/get-section-id" import { useCallback, useEffect, useMemo, useRef, useState } from "react" import dynamic from "next/dynamic" import { useInView } from "react-intersection-observer" -import { useSidebar } from "docs-ui" +import { isElmWindow, useScrollController, useSidebar } from "docs-ui" import type { TagOperationCodeSectionProps } from "./CodeSection" import TagsOperationDescriptionSection from "./DescriptionSection" import DividedLayout from "@/layouts/Divided" @@ -40,10 +40,14 @@ const TagOperation = ({ ) const nodeRef = useRef(null) const { loading, removeLoading } = useLoading() + const { scrollableElement } = useScrollController() + const root = useMemo(() => { + return isElmWindow(scrollableElement) ? document.body : scrollableElement + }, [scrollableElement]) const { ref } = useInView({ threshold: 0.3, rootMargin: `112px 0px 112px 0px`, - root: document.getElementById("main") || document.body, + root, onChange: (changedInView) => { if (changedInView) { if (!show) { diff --git a/www/apps/api-reference/components/Tags/Section/index.tsx b/www/apps/api-reference/components/Tags/Section/index.tsx index 02fbcdd95fe2e..f4c37f9105bf9 100644 --- a/www/apps/api-reference/components/Tags/Section/index.tsx +++ b/www/apps/api-reference/components/Tags/Section/index.tsx @@ -4,7 +4,7 @@ import getSectionId from "@/utils/get-section-id" import type { OpenAPIV3 } from "openapi-types" import { useInView } from "react-intersection-observer" import { useEffect, useMemo, useState } from "react" -import { useSidebar } from "docs-ui" +import { isElmWindow, useScrollController, useSidebar } from "docs-ui" import dynamic from "next/dynamic" import type { SectionProps } from "../../Section" import type { MDXContentClientProps } from "../../MDXContent/Client" @@ -40,10 +40,14 @@ const TagSection = ({ tag }: TagSectionProps) => { const slugTagName = useMemo(() => getSectionId([tag.name]), [tag]) const { area } = useArea() const pathname = usePathname() + const { scrollableElement } = useScrollController() + const root = useMemo(() => { + return isElmWindow(scrollableElement) ? document.body : scrollableElement + }, [scrollableElement]) const { ref } = useInView({ threshold: 0.5, rootMargin: `112px 0px 112px 0px`, - root: document.getElementById("main") || document.body, + root, onChange: (inView) => { if (inView && !loadPaths) { setLoadPaths(true) diff --git a/www/apps/api-reference/providers/index.tsx b/www/apps/api-reference/providers/index.tsx index 3369a78c2efec..dc0dc486135ee 100644 --- a/www/apps/api-reference/providers/index.tsx +++ b/www/apps/api-reference/providers/index.tsx @@ -25,15 +25,15 @@ const Providers = ({ children }: ProvidersProps) => { - - - + + + {children} - - - + + + diff --git a/www/apps/api-reference/providers/sidebar.tsx b/www/apps/api-reference/providers/sidebar.tsx index 2ed106bc2d109..6650e86ccaa42 100644 --- a/www/apps/api-reference/providers/sidebar.tsx +++ b/www/apps/api-reference/providers/sidebar.tsx @@ -1,5 +1,9 @@ "use client" -import { SidebarProvider as UiSidebarProvider, usePageLoading } from "docs-ui" +import { + SidebarProvider as UiSidebarProvider, + usePageLoading, + useScrollController, +} from "docs-ui" type SidebarProviderProps = { children?: React.ReactNode @@ -7,12 +11,14 @@ type SidebarProviderProps = { const SidebarProvider = ({ children }: SidebarProviderProps) => { const { isLoading, setIsLoading } = usePageLoading() + const { scrollableElement } = useScrollController() return ( { - - - - - {children} - - - - + + + + {children} + + + diff --git a/www/apps/ui/src/providers/sidebar.tsx b/www/apps/ui/src/providers/sidebar.tsx index 828c6bb2dbf20..54c8946bb05dd 100644 --- a/www/apps/ui/src/providers/sidebar.tsx +++ b/www/apps/ui/src/providers/sidebar.tsx @@ -1,4 +1,7 @@ -import { SidebarProvider as UiSidebarProvider } from "docs-ui" +import { + SidebarProvider as UiSidebarProvider, + useScrollController, +} from "docs-ui" import { docsConfig } from "@/config/docs" type SidebarProviderProps = { @@ -6,10 +9,12 @@ type SidebarProviderProps = { } const SidebarProvider = ({ children }: SidebarProviderProps) => { + const { scrollableElement } = useScrollController() return ( {children} diff --git a/www/packages/docs-ui/src/components/Sidebar/Item/index.tsx b/www/packages/docs-ui/src/components/Sidebar/Item/index.tsx index 53165303d1b73..7eb0768713d8f 100644 --- a/www/packages/docs-ui/src/components/Sidebar/Item/index.tsx +++ b/www/packages/docs-ui/src/components/Sidebar/Item/index.tsx @@ -22,9 +22,10 @@ export const SidebarItem = ({ }: SidebarItemProps) => { const [showLoading, setShowLoading] = useState(false) const { isItemActive, setMobileSidebarOpen: setSidebarOpen } = useSidebar() - const active = useMemo(() => { - return isItemActive(item, nested) - }, [isItemActive, item, nested]) + const active = useMemo( + () => isItemActive(item, nested), + [isItemActive, item, nested] + ) const collapsed = !expandItems && !isItemActive(item, true) const ref = useRef(null) diff --git a/www/packages/docs-ui/src/hooks/use-scroll-utils/index.tsx b/www/packages/docs-ui/src/hooks/use-scroll-utils/index.tsx index 73805c19f9bcc..3419eb5536b48 100644 --- a/www/packages/docs-ui/src/hooks/use-scroll-utils/index.tsx +++ b/www/packages/docs-ui/src/hooks/use-scroll-utils/index.tsx @@ -17,7 +17,9 @@ import React, { useMemo, useRef, type ReactNode, + useState, } from "react" +import { getScrolledTop as getScrolledTopUtil } from "../../utils" type EventFunc = (...args: never[]) => unknown @@ -54,7 +56,9 @@ type ScrollController = { /** Disable scroll events in `useScrollPosition`. */ disableScrollEvents: () => void /** 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( @@ -62,9 +66,19 @@ function useScrollControllerContextValue( ): ScrollController { const scrollEventsEnabledRef = useRef(true) - const getScrollableElement = useCallback(() => { - return (document.querySelector(scrollableSelector) as Element) || window - }, [scrollableSelector]) + const [scrollableElement, setScrollableElement] = useState< + Element | Window | undefined + >() + + useEffect(() => { + setScrollableElement( + (document.querySelector(scrollableSelector) as Element) || window + ) + }, []) + + const getScrolledTop = () => { + return scrollableElement ? getScrolledTopUtil(scrollableElement) : 0 + } return useMemo( () => ({ @@ -75,9 +89,10 @@ function useScrollControllerContextValue( disableScrollEvents: () => { scrollEventsEnabledRef.current = false }, - getScrollableElement, + scrollableElement, + getScrolledTop, }), - [getScrollableElement] + [scrollableElement] ) } @@ -176,7 +191,7 @@ type UseScrollPositionSaver = { } function useScrollPositionSaver(): UseScrollPositionSaver { - const { getScrollableElement } = useScrollController() + const { scrollableElement } = useScrollController() const lastElementRef = useRef<{ elem: HTMLElement | null; top: number }>({ elem: null, top: 0, @@ -199,7 +214,7 @@ function useScrollPositionSaver(): UseScrollPositionSaver { const newTop = elem.getBoundingClientRect().top const heightDiff = newTop - top if (heightDiff) { - getScrollableElement().scrollBy({ left: 0, top: heightDiff }) + scrollableElement?.scrollBy({ left: 0, top: heightDiff }) } lastElementRef.current = { elem: null, top: 0 } diff --git a/www/packages/docs-ui/src/providers/Sidebar/index.tsx b/www/packages/docs-ui/src/providers/Sidebar/index.tsx index 7c2b0523a493c..c4370fd612b9b 100644 --- a/www/packages/docs-ui/src/providers/Sidebar/index.tsx +++ b/www/packages/docs-ui/src/providers/Sidebar/index.tsx @@ -10,6 +10,7 @@ import React, { useState, } from "react" import { usePathname } from "next/navigation" +import { getScrolledTop } from "../../utils" export enum SidebarItemSections { TOP = "top", @@ -129,6 +130,7 @@ export type SidebarProviderProps = { initialItems?: SidebarSectionItemsType shouldHandleHashChange?: boolean shouldHandlePathChange?: boolean + scrollableElement?: Element | Window } export const SidebarProvider = ({ @@ -138,6 +140,7 @@ export const SidebarProvider = ({ initialItems, shouldHandleHashChange = false, shouldHandlePathChange = false, + scrollableElement, }: SidebarProviderProps) => { const [items, dispatch] = useReducer(reducer, { top: initialItems?.top || [], @@ -148,6 +151,9 @@ export const SidebarProvider = ({ const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false) const [desktopSidebarOpen, setDesktopSidebarOpen] = useState(true) const pathname = usePathname() + const getResolvedScrollableElement = useCallback(() => { + return scrollableElement || window + }, [scrollableElement]) const findItemInSection = useCallback( ( @@ -249,15 +255,21 @@ export const SidebarProvider = ({ } }, [activePath]) + useEffect(() => { + if (shouldHandleHashChange) { + init() + } + }, [shouldHandleHashChange]) + useEffect(() => { if (!shouldHandleHashChange) { return } - init() + const resolvedScrollableElement = getResolvedScrollableElement() const handleScroll = () => { - if (window.scrollY === 0) { + if (getScrolledTop(resolvedScrollableElement) === 0) { setActivePath("") // can't use next router as it doesn't support // changing url without scrolling @@ -265,14 +277,17 @@ export const SidebarProvider = ({ } } - window.addEventListener("scroll", handleScroll) - window.addEventListener("hashchange", handleHashChange) + resolvedScrollableElement.addEventListener("scroll", handleScroll) + resolvedScrollableElement.addEventListener("hashchange", handleHashChange) return () => { - window.removeEventListener("scroll", handleScroll) - window.removeEventListener("hashchange", handleHashChange) + resolvedScrollableElement.removeEventListener("scroll", handleScroll) + resolvedScrollableElement.removeEventListener( + "hashchange", + handleHashChange + ) } - }, [handleHashChange, shouldHandleHashChange]) + }, [handleHashChange, shouldHandleHashChange, getResolvedScrollableElement]) useEffect(() => { if (isLoading && items.top.length && items.bottom.length) { diff --git a/www/packages/docs-ui/src/utils/get-scrolled-top.ts b/www/packages/docs-ui/src/utils/get-scrolled-top.ts new file mode 100644 index 0000000000000..0a36b588db147 --- /dev/null +++ b/www/packages/docs-ui/src/utils/get-scrolled-top.ts @@ -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 +} diff --git a/www/packages/docs-ui/src/utils/index.ts b/www/packages/docs-ui/src/utils/index.ts index 1fd3db1177ece..3511a0f45e00d 100644 --- a/www/packages/docs-ui/src/utils/index.ts +++ b/www/packages/docs-ui/src/utils/index.ts @@ -3,4 +3,6 @@ export * from "./capitalize" export * from "./check-sidebar-item-visibility" export * from "./dom-utils" export * from "./format-report-link" +export * from "./get-scrolled-top" +export * from "./is-elm-window" export * from "./swr-fetcher" diff --git a/www/packages/docs-ui/src/utils/is-elm-window.ts b/www/packages/docs-ui/src/utils/is-elm-window.ts new file mode 100644 index 0000000000000..f4c70bc040cd4 --- /dev/null +++ b/www/packages/docs-ui/src/utils/is-elm-window.ts @@ -0,0 +1,3 @@ +export function isElmWindow(elm: unknown): elm is Window { + return typeof window !== "undefined" && elm === window +}