Files
supabase/packages/ui-patterns/Toc/toc-primitive.tsx
T
Francesco Sansalvadore d24df14aab Feat/toc component (#33964)
* test fuma toc

* make toc work

* ok toc

* ui-patterns toc

* fix types

* cleanup

* fix overflow issues

* cleanup

* fix loading w

* Update apps/docs/components/GuidesTableOfContents.tsx

* fix prettier

* format

* install deps

* target ES2018

* tweak regex

* update docs test snapshots

* fix useSubscribeTocRerender
2025-03-05 23:10:02 +01:00

116 lines
2.7 KiB
TypeScript

'use client'
import { createContext, forwardRef, useContext, useMemo, useRef } from 'react'
import scrollIntoView from 'scroll-into-view-if-needed'
import type { AnchorHTMLAttributes, ReactNode, RefObject } from 'react'
import type { TableOfContents } from './server/get-toc'
import { mergeRefs, useAnchorObserver, useOnChange } from 'common'
export const ActiveAnchorContext = createContext<string[]>([])
const ScrollContext = createContext<RefObject<HTMLElement | null>>({
current: null,
})
/**
* The estimated active heading ID
*/
export function useActiveAnchor(): string | undefined {
return useContext(ActiveAnchorContext).at(-1)
}
/**
* The id of visible anchors
*/
export function useActiveAnchors(): string[] {
return useContext(ActiveAnchorContext)
}
export interface AnchorProviderProps {
toc: TableOfContents
/**
* Only accept one active item at most
*
* @defaultValue true
*/
single?: boolean
children?: ReactNode
}
export interface ScrollProviderProps {
/**
* Scroll into the view of container when active
*/
containerRef: RefObject<HTMLElement | null>
children?: ReactNode
}
export function ScrollProvider({
containerRef,
children,
}: ScrollProviderProps): React.ReactElement {
return <ScrollContext.Provider value={containerRef}>{children}</ScrollContext.Provider>
}
export function AnchorProvider({
toc,
single = true,
children,
}: AnchorProviderProps): React.ReactElement {
const headings = useMemo(() => {
return toc.map((item) => item.url.split('#')[1])
}, [toc])
return (
<ActiveAnchorContext.Provider value={useAnchorObserver(headings, single)}>
{children}
</ActiveAnchorContext.Provider>
)
}
export interface TOCItemProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
href: string
onActiveChange?: (v: boolean) => void
}
export const TOCItem = forwardRef<HTMLAnchorElement, TOCItemProps>(
({ onActiveChange, ...props }, ref) => {
const containerRef = useContext(ScrollContext)
const anchors = useActiveAnchors()
const anchorRef = useRef<HTMLAnchorElement>(null)
const mergedRef = mergeRefs(anchorRef, ref)
const isActive = anchors.includes(props.href.slice(1))
useOnChange(isActive, (v) => {
const element = anchorRef.current
if (!element) return
if (v && containerRef.current) {
scrollIntoView(element, {
behavior: 'smooth',
block: 'center',
inline: 'center',
scrollMode: 'always',
boundary: containerRef.current,
})
}
onActiveChange?.(v)
})
return (
<a ref={mergedRef} data-active={isActive} {...props}>
{props.children}
</a>
)
}
)
TOCItem.displayName = 'TOCItem'