diff --git a/www/apps/api-reference/components/MDXComponents/H2/index.tsx b/www/apps/api-reference/components/MDXComponents/H2/index.tsx
index 8430a09d6a..002814272d 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 3ad5075b3b..3eeaeb2cb5 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 02fbcdd95f..f4c37f9105 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 3369a78c2e..dc0dc48613 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 2ed106bc2d..6650e86cca 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 828c6bb2db..54c8946bb0 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 53165303d1..7eb0768713 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 73805c19f9..3419eb5536 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 7c2b0523a4..c4370fd612 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 0000000000..0a36b588db
--- /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 1fd3db1177..3511a0f45e 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 0000000000..f4c70bc040
--- /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
+}