docs: redesign sidebar (#8408)
* initial changes * redesign the sidebar + nav drawer * changes to sidebar items * finish up sidebar redesign * support new sidebar in resources * general fixes * integrate in ui * support api reference * refactor * integrate in user guide * docs: fix build errors * fix user guide build * more refactoring * added banner * added bottom logo + icon * fix up sidebar * fix up paddings * fix shadow bottom * docs: add table of content (#8445) * add toc types * implement toc functionality * finished toc redesign * redesigned table of content * mobile fixes * truncate text in toc * mobile fixes * merge fixes * implement redesign * add hide sidebar * add menu action item * finish up hide sidebar design * implement redesign in resources * integrate in api reference * integrate changes in ui * fixes to api reference scrolling * fix build error * fix build errors * fixes * fixes to sidebar * general fixes * fix active category not closing * fix long titles
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
export * from "./use-active-on-scroll"
|
||||
export * from "./use-click-outside"
|
||||
export * from "./use-collapsible"
|
||||
export * from "./use-collapsible-code-lines"
|
||||
export * from "./use-copy"
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { isElmWindow, useIsBrowser, useScrollController } from "../.."
|
||||
import { usePathname } from "next/navigation"
|
||||
import { useMutationObserver } from "../use-mutation-observer"
|
||||
|
||||
export type ActiveOnScrollItem = {
|
||||
heading: HTMLHeadingElement
|
||||
children?: ActiveOnScrollItem[]
|
||||
}
|
||||
|
||||
export type UseActiveOnScrollProps = {
|
||||
rootElm?: Document | HTMLElement
|
||||
enable?: boolean
|
||||
useDefaultIfNoActive?: boolean
|
||||
}
|
||||
|
||||
export const useActiveOnScroll = ({
|
||||
rootElm,
|
||||
enable = true,
|
||||
useDefaultIfNoActive = true,
|
||||
}: UseActiveOnScrollProps) => {
|
||||
const [items, setItems] = useState<ActiveOnScrollItem[]>([])
|
||||
const [activeItemId, setActiveItemId] = useState("")
|
||||
const { scrollableElement } = useScrollController()
|
||||
const isBrowser = useIsBrowser()
|
||||
const pathname = usePathname()
|
||||
const root = useMemo(() => {
|
||||
if (!enable) {
|
||||
return
|
||||
}
|
||||
if (rootElm) {
|
||||
return rootElm
|
||||
}
|
||||
|
||||
if (!isBrowser) {
|
||||
return
|
||||
}
|
||||
|
||||
return document
|
||||
}, [rootElm, isBrowser, enable])
|
||||
const getHeadingsInElm = useCallback(() => {
|
||||
if (!isBrowser || !enable) {
|
||||
return []
|
||||
}
|
||||
|
||||
return root?.querySelectorAll("h2,h3")
|
||||
}, [isBrowser, pathname, root, enable])
|
||||
const setHeadingItems = useCallback(() => {
|
||||
if (!enable) {
|
||||
return
|
||||
}
|
||||
const headings = getHeadingsInElm()
|
||||
const itemsToSet: ActiveOnScrollItem[] = []
|
||||
let lastLevel2HeadingIndex = -1
|
||||
|
||||
headings?.forEach((heading) => {
|
||||
const level = parseInt(heading.tagName.replace("H", ""))
|
||||
const isLevel2 = level === 2
|
||||
const headingItem: ActiveOnScrollItem = {
|
||||
heading: heading as HTMLHeadingElement,
|
||||
children: [],
|
||||
}
|
||||
|
||||
if (isLevel2 || lastLevel2HeadingIndex === -1) {
|
||||
itemsToSet.push(headingItem)
|
||||
if (isLevel2) {
|
||||
lastLevel2HeadingIndex = itemsToSet.length - 1
|
||||
}
|
||||
} else if (lastLevel2HeadingIndex !== -1) {
|
||||
itemsToSet[lastLevel2HeadingIndex].children?.push(headingItem)
|
||||
}
|
||||
})
|
||||
|
||||
setItems(itemsToSet)
|
||||
}, [getHeadingsInElm, enable])
|
||||
|
||||
useMutationObserver({
|
||||
elm: root,
|
||||
callback: setHeadingItems,
|
||||
})
|
||||
|
||||
const setActiveToClosest = useCallback(() => {
|
||||
if (!enable) {
|
||||
return
|
||||
}
|
||||
const headings = getHeadingsInElm()
|
||||
let closestPositiveHeading: HTMLHeadingElement | undefined = undefined
|
||||
let closestNegativeHeading: HTMLHeadingElement | undefined = undefined
|
||||
let closestPositiveDistance = Infinity
|
||||
let closestNegativeDistance = -Infinity
|
||||
const halfway = isElmWindow(scrollableElement)
|
||||
? scrollableElement.innerHeight / 2
|
||||
: scrollableElement
|
||||
? scrollableElement.scrollHeight / 2
|
||||
: 0
|
||||
|
||||
headings?.forEach((heading) => {
|
||||
const headingDistance = heading.getBoundingClientRect().top
|
||||
|
||||
if (headingDistance > 0 && headingDistance < closestPositiveDistance) {
|
||||
closestPositiveDistance = headingDistance
|
||||
closestPositiveHeading = heading as HTMLHeadingElement
|
||||
} else if (
|
||||
headingDistance < 0 &&
|
||||
headingDistance > closestNegativeDistance
|
||||
) {
|
||||
closestNegativeDistance = headingDistance
|
||||
closestNegativeHeading = heading as HTMLHeadingElement
|
||||
}
|
||||
})
|
||||
|
||||
const negativeDistanceToHalfway = Math.abs(
|
||||
halfway + closestNegativeDistance
|
||||
)
|
||||
const positiveDistanceToHalfway = Math.abs(
|
||||
halfway - closestPositiveDistance
|
||||
)
|
||||
|
||||
const chosenClosest =
|
||||
negativeDistanceToHalfway > positiveDistanceToHalfway
|
||||
? closestNegativeHeading
|
||||
: closestPositiveHeading
|
||||
|
||||
setActiveItemId(
|
||||
chosenClosest
|
||||
? (chosenClosest as HTMLHeadingElement).id
|
||||
: items.length
|
||||
? useDefaultIfNoActive
|
||||
? items[0].heading.id
|
||||
: ""
|
||||
: ""
|
||||
)
|
||||
}, [getHeadingsInElm, items, enable])
|
||||
|
||||
useEffect(() => {
|
||||
if (!scrollableElement || !enable) {
|
||||
return
|
||||
}
|
||||
|
||||
scrollableElement.addEventListener("scroll", setActiveToClosest)
|
||||
|
||||
return () => {
|
||||
scrollableElement.removeEventListener("scroll", setActiveToClosest)
|
||||
}
|
||||
}, [scrollableElement, setActiveToClosest, enable])
|
||||
|
||||
useEffect(() => {
|
||||
if (items.length && enable) {
|
||||
setActiveToClosest()
|
||||
}
|
||||
}, [items, setActiveToClosest, enable])
|
||||
|
||||
return {
|
||||
items,
|
||||
activeItemId,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client"
|
||||
|
||||
import React, { useCallback, useEffect } from "react"
|
||||
import { useIsBrowser } from "../.."
|
||||
|
||||
export type UseClickOutsideProps = {
|
||||
elmRef: React.RefObject<HTMLElement>
|
||||
onClickOutside: (e: MouseEvent) => void
|
||||
}
|
||||
|
||||
export const useClickOutside = ({
|
||||
elmRef,
|
||||
onClickOutside,
|
||||
}: UseClickOutsideProps) => {
|
||||
const isBrowser = useIsBrowser()
|
||||
|
||||
const checkClickOutside = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (!elmRef.current?.contains(e.target as Node)) {
|
||||
onClickOutside(e)
|
||||
}
|
||||
},
|
||||
[elmRef.current, onClickOutside]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isBrowser) {
|
||||
return
|
||||
}
|
||||
|
||||
window.document.addEventListener("click", checkClickOutside)
|
||||
|
||||
return () => {
|
||||
window.document.removeEventListener("click", checkClickOutside)
|
||||
}
|
||||
}, [isBrowser, checkClickOutside])
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useEffect } from "react"
|
||||
|
||||
type UseMutationObserverProps = {
|
||||
elm: Document | HTMLElement | undefined
|
||||
callback: () => void
|
||||
options?: {
|
||||
attributes?: boolean
|
||||
characterData?: boolean
|
||||
childList?: boolean
|
||||
subtree?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const useMutationObserver = ({
|
||||
elm,
|
||||
callback,
|
||||
options = {
|
||||
attributes: true,
|
||||
characterData: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
},
|
||||
}: UseMutationObserverProps) => {
|
||||
useEffect(() => {
|
||||
if (elm) {
|
||||
const observer = new MutationObserver(callback)
|
||||
observer.observe(elm, options)
|
||||
return () => observer.disconnect()
|
||||
}
|
||||
}, [callback, options])
|
||||
}
|
||||
@@ -59,6 +59,10 @@ type ScrollController = {
|
||||
scrollableElement: Element | Window | undefined
|
||||
/** Retrieves the scroll top if the scrollable element */
|
||||
getScrolledTop: () => number
|
||||
/** Scrolls to an element */
|
||||
scrollToElement: (elm: HTMLElement) => void
|
||||
/** Scrolls to a top value */
|
||||
scrollToTop: (top: number, parentTop?: number) => void
|
||||
}
|
||||
|
||||
function useScrollControllerContextValue({
|
||||
@@ -83,6 +87,27 @@ function useScrollControllerContextValue({
|
||||
return scrollableElement ? getScrolledTopUtil(scrollableElement) : 0
|
||||
}
|
||||
|
||||
const scrollToElement = (elm: HTMLElement) => {
|
||||
scrollToTop(elm.offsetTop)
|
||||
}
|
||||
|
||||
const scrollToTop = (top: number, parentTop?: number) => {
|
||||
const parentOffsetTop =
|
||||
parentTop !== undefined
|
||||
? parentTop
|
||||
: isElmWindow(scrollableElement)
|
||||
? 0
|
||||
: scrollableElement instanceof HTMLElement
|
||||
? scrollableElement.offsetTop
|
||||
: 0
|
||||
|
||||
scrollableElement?.scrollTo({
|
||||
// 56 is the height of the navbar
|
||||
// might need a better way to determine it.
|
||||
top: top - parentOffsetTop - 56,
|
||||
})
|
||||
}
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
scrollEventsEnabledRef,
|
||||
@@ -94,6 +119,8 @@ function useScrollControllerContextValue({
|
||||
},
|
||||
scrollableElement,
|
||||
getScrolledTop,
|
||||
scrollToElement,
|
||||
scrollToTop,
|
||||
}),
|
||||
[scrollableElement]
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user