mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 06:31:49 +00:00
Core: Change usage of TOC component
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
---
|
||||
'fumadocs-core': major
|
||||
---
|
||||
|
||||
**Change usage of TOC component**
|
||||
|
||||
**why:** Improve the flexibility of headless components
|
||||
|
||||
**migrate:**
|
||||
|
||||
Instead of
|
||||
|
||||
```tsx
|
||||
import * as Base from 'fumadocs-core/toc';
|
||||
|
||||
return (
|
||||
<Base.TOCProvider>
|
||||
<Base.TOCItem />
|
||||
</Base.TOCProvider>
|
||||
);
|
||||
```
|
||||
|
||||
Use
|
||||
|
||||
```tsx
|
||||
import * as Base from 'fumadocs-core/toc';
|
||||
|
||||
return (
|
||||
<Base.AnchorProvider>
|
||||
<Base.ScrollProvider>
|
||||
<Base.TOCItem/>
|
||||
<Base.TOCItem/>
|
||||
</Base.ScrollProvider>
|
||||
</Base.AnchorProvider>
|
||||
);
|
||||
```
|
||||
@@ -11,41 +11,41 @@ A Table of Contents with active anchor observer and auto scroll.
|
||||
import * as Base from 'fumadocs-core/toc';
|
||||
|
||||
return (
|
||||
<Base.TOCProvider>
|
||||
<Base.TOCItem />
|
||||
</Base.TOCProvider>
|
||||
<Base.AnchorProvider>
|
||||
<Base.ScrollProvider>
|
||||
<Base.TOCItem />
|
||||
<Base.TOCItem />
|
||||
</Base.ScrollProvider>
|
||||
</Base.AnchorProvider>
|
||||
);
|
||||
```
|
||||
|
||||
### TOC Provider
|
||||
### Anchor Provider
|
||||
|
||||
Contains all anchor items. By default, it automatically scrolls to the active
|
||||
anchor.
|
||||
Watch for the active anchor using the Intersection API.
|
||||
|
||||
```json doc-gen:typescript
|
||||
{
|
||||
"file": "./content/docs/headless/props.ts",
|
||||
"name": "TOCProviderProps"
|
||||
"name": "AnchorProviderProps"
|
||||
}
|
||||
```
|
||||
|
||||
### Scroll Provider
|
||||
|
||||
Contains all TOC items. It automatically scrolls to the active anchor.
|
||||
|
||||
```json doc-gen:typescript
|
||||
{
|
||||
"file": "./content/docs/headless/props.ts",
|
||||
"name": "ScrollProviderProps"
|
||||
}
|
||||
```
|
||||
|
||||
### TOC Item
|
||||
|
||||
The anchor element.
|
||||
The item to jump to the anchor.
|
||||
|
||||
| Data Attribute | Values | Description |
|
||||
| -------------- | ------------- | ---------------- |
|
||||
| `data-active` | `true, false` | Is active anchor |
|
||||
|
||||
### Use Active Anchor
|
||||
|
||||
A hook for getting the active anchor. It accepts the item url and return a
|
||||
boolean value, must be called under the TOC Provider.
|
||||
|
||||
Useful for implementing advanced styling.
|
||||
|
||||
```ts
|
||||
import { useActiveAnchor } from 'fumadocs-core/toc';
|
||||
|
||||
const isActive = useActiveAnchor(item.url);
|
||||
```
|
||||
| `data-active` | `true, false` | Is anchor active |
|
||||
|
||||
@@ -15,10 +15,8 @@ export type BreadcrumbItem = Breadcrumb.BreadcrumbItem;
|
||||
export type SidebarProviderProps = Sidebar.SidebarProviderProps;
|
||||
export type SidebarTriggerProps = Sidebar.SidebarTriggerProps<ElementType>;
|
||||
|
||||
export type TOCProviderProps = Omit<
|
||||
ComponentPropsWithoutRef<typeof TOC.TOCProvider>,
|
||||
keyof ComponentPropsWithoutRef<'div'>
|
||||
>;
|
||||
export type ScrollProviderProps = TOC.ScrollProviderProps;
|
||||
export type AnchorProviderProps = TOC.AnchorProviderProps;
|
||||
|
||||
export type TOCItemType = Server.TOCItemType;
|
||||
|
||||
|
||||
@@ -24,10 +24,6 @@
|
||||
"import": "./dist/toc.js",
|
||||
"types": "./dist/toc.d.ts"
|
||||
},
|
||||
"./toc-internal": {
|
||||
"import": "./dist/toc-internal.js",
|
||||
"types": "./dist/toc-internal.d.ts"
|
||||
},
|
||||
"./search/client": {
|
||||
"import": "./dist/search/client.js",
|
||||
"types": "./dist/search/client.d.ts"
|
||||
@@ -84,9 +80,6 @@
|
||||
"toc": [
|
||||
"./dist/toc.d.ts"
|
||||
],
|
||||
"toc-internal": [
|
||||
"./dist/toc-internal.d.ts"
|
||||
],
|
||||
"search/client": [
|
||||
"./dist/search/client.d.ts"
|
||||
],
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import type { AnchorHTMLAttributes, ReactNode, RefObject } from 'react';
|
||||
import { createContext, forwardRef, useContext, useMemo, useRef } from 'react';
|
||||
import scrollIntoView from 'scroll-into-view-if-needed';
|
||||
import type { TableOfContents } from '@/server/get-toc';
|
||||
import { mergeRefs } from '@/utils/merge-refs';
|
||||
import { useOnChange } from '@/utils/use-on-change';
|
||||
import { useAnchorObserver } from './utils/use-anchor-observer';
|
||||
|
||||
const ActiveAnchorContext = createContext<string | undefined>(undefined);
|
||||
|
||||
const ScrollContext = createContext<RefObject<HTMLElement>>({ current: null });
|
||||
|
||||
/**
|
||||
* The id of active anchor (doesn't include hash)
|
||||
*/
|
||||
export function useActiveAnchor(): string | undefined {
|
||||
return useContext(ActiveAnchorContext);
|
||||
}
|
||||
|
||||
export interface AnchorProviderProps {
|
||||
toc: TableOfContents;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export interface ScrollProviderProps {
|
||||
/**
|
||||
* Scroll into the view of container when active
|
||||
*/
|
||||
containerRef: RefObject<HTMLElement>;
|
||||
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export function ScrollProvider({
|
||||
containerRef,
|
||||
children,
|
||||
}: ScrollProviderProps): React.ReactElement {
|
||||
return (
|
||||
<ScrollContext.Provider value={containerRef}>
|
||||
{children}
|
||||
</ScrollContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function AnchorProvider({
|
||||
toc,
|
||||
children,
|
||||
}: AnchorProviderProps): React.ReactElement {
|
||||
const headings = useMemo(() => {
|
||||
return toc.map((item) => item.url.split('#')[1]);
|
||||
}, [toc]);
|
||||
|
||||
const activeAnchor = useAnchorObserver(headings);
|
||||
|
||||
return (
|
||||
<ActiveAnchorContext.Provider value={activeAnchor}>
|
||||
{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 activeAnchor = useActiveAnchor();
|
||||
const anchorRef = useRef<HTMLAnchorElement>(null);
|
||||
const mergedRef = mergeRefs(anchorRef, ref);
|
||||
|
||||
const isActive = activeAnchor === props.href.split('#')[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';
|
||||
+48
-64
@@ -1,70 +1,51 @@
|
||||
import type {
|
||||
AnchorHTMLAttributes,
|
||||
HTMLAttributes,
|
||||
ReactNode,
|
||||
RefObject,
|
||||
} from 'react';
|
||||
import {
|
||||
createContext,
|
||||
forwardRef,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import type { AnchorHTMLAttributes, ReactNode, RefObject } from 'react';
|
||||
import { createContext, forwardRef, useContext, useMemo, useRef } from 'react';
|
||||
import scrollIntoView from 'scroll-into-view-if-needed';
|
||||
import type { TableOfContents } from '@/server/get-toc';
|
||||
import { mergeRefs } from '@/utils/merge-refs';
|
||||
import { useOnChange } from '@/utils/use-on-change';
|
||||
import { useAnchorObserver } from './utils/use-anchor-observer';
|
||||
|
||||
const ActiveAnchorContext = createContext<{
|
||||
activeAnchor?: string;
|
||||
containerRef?: RefObject<HTMLElement>;
|
||||
}>({});
|
||||
const ActiveAnchorContext = createContext<string | undefined>(undefined);
|
||||
|
||||
export const useActiveAnchor = (url: string): boolean => {
|
||||
const { activeAnchor } = useContext(ActiveAnchorContext);
|
||||
const ScrollContext = createContext<RefObject<HTMLElement>>({ current: null });
|
||||
|
||||
return activeAnchor === url.split('#')[1];
|
||||
};
|
||||
|
||||
export interface TOCProviderProps extends HTMLAttributes<HTMLDivElement> {
|
||||
toc: TableOfContents;
|
||||
/**
|
||||
* The id of active anchor (doesn't include hash)
|
||||
*/
|
||||
export function useActiveAnchor(): string | undefined {
|
||||
return useContext(ActiveAnchorContext);
|
||||
}
|
||||
|
||||
export interface TOCScrollProvider {
|
||||
export interface AnchorProviderProps {
|
||||
toc: TableOfContents;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export interface ScrollProviderProps {
|
||||
/**
|
||||
* Scroll into the view of container when active
|
||||
*/
|
||||
containerRef?: RefObject<HTMLElement>;
|
||||
containerRef: RefObject<HTMLElement>;
|
||||
|
||||
toc: TableOfContents;
|
||||
children: ReactNode;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
// todo: remove in next major
|
||||
export const TOCProvider = forwardRef<HTMLDivElement, TOCProviderProps>(
|
||||
({ toc, ...props }, ref) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const mergedRef = mergeRefs(containerRef, ref);
|
||||
|
||||
return (
|
||||
<div ref={mergedRef} {...props}>
|
||||
<TOCScrollProvider toc={toc} containerRef={containerRef}>
|
||||
{props.children}
|
||||
</TOCScrollProvider>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
TOCProvider.displayName = 'TOCProvider';
|
||||
|
||||
export function TOCScrollProvider({
|
||||
toc,
|
||||
export function ScrollProvider({
|
||||
containerRef,
|
||||
children,
|
||||
}: TOCScrollProvider): React.ReactElement {
|
||||
}: ScrollProviderProps): React.ReactElement {
|
||||
return (
|
||||
<ScrollContext.Provider value={containerRef}>
|
||||
{children}
|
||||
</ScrollContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function AnchorProvider({
|
||||
toc,
|
||||
children,
|
||||
}: AnchorProviderProps): React.ReactElement {
|
||||
const headings = useMemo(() => {
|
||||
return toc.map((item) => item.url.split('#')[1]);
|
||||
}, [toc]);
|
||||
@@ -72,32 +53,33 @@ export function TOCScrollProvider({
|
||||
const activeAnchor = useAnchorObserver(headings);
|
||||
|
||||
return (
|
||||
<ActiveAnchorContext.Provider value={{ containerRef, activeAnchor }}>
|
||||
<ActiveAnchorContext.Provider value={activeAnchor}>
|
||||
{children}
|
||||
</ActiveAnchorContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export interface TOCItemProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||
export interface TOCItemProps
|
||||
extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
|
||||
href: string;
|
||||
|
||||
/**
|
||||
* Scroll into the view of container when active
|
||||
*/
|
||||
containerRef?: RefObject<HTMLElement>;
|
||||
onActiveChange?: (v: boolean) => void;
|
||||
}
|
||||
|
||||
export const TOCItem = forwardRef<HTMLAnchorElement, TOCItemProps>(
|
||||
({ containerRef: container, ...props }, ref) => {
|
||||
const { containerRef = container } = useContext(ActiveAnchorContext);
|
||||
const active = useActiveAnchor(props.href);
|
||||
({ onActiveChange, ...props }, ref) => {
|
||||
const containerRef = useContext(ScrollContext);
|
||||
const activeAnchor = useActiveAnchor();
|
||||
const anchorRef = useRef<HTMLAnchorElement>(null);
|
||||
const mergedRef = mergeRefs(anchorRef, ref);
|
||||
|
||||
useEffect(() => {
|
||||
const element = anchorRef.current;
|
||||
const isActive = activeAnchor === props.href.split('#')[1];
|
||||
|
||||
if (active && element && containerRef) {
|
||||
useOnChange(isActive, (v) => {
|
||||
const element = anchorRef.current;
|
||||
if (!element) return;
|
||||
|
||||
if (v && containerRef.current) {
|
||||
scrollIntoView(element, {
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
@@ -106,10 +88,12 @@ export const TOCItem = forwardRef<HTMLAnchorElement, TOCItemProps>(
|
||||
boundary: containerRef.current,
|
||||
});
|
||||
}
|
||||
}, [active, containerRef]);
|
||||
|
||||
onActiveChange?.(v);
|
||||
});
|
||||
|
||||
return (
|
||||
<a ref={mergedRef} data-active={active} {...props}>
|
||||
<a ref={mergedRef} data-active={isActive} {...props}>
|
||||
{props.children}
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ export default defineConfig({
|
||||
target: 'es2022',
|
||||
format: 'esm',
|
||||
entry: [
|
||||
'src/{toc,toc-internal,link,breadcrumb,sidebar,dynamic-link}.tsx',
|
||||
'src/{toc,link,breadcrumb,sidebar,dynamic-link}.tsx',
|
||||
'src/{server,source,mdx-plugins}/index.ts',
|
||||
'src/{search,search-algolia}/client.ts',
|
||||
'src/search/shared.ts',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChevronRight, Text } from 'lucide-react';
|
||||
import type { TOCItemType } from 'fumadocs-core/server';
|
||||
import * as Primitive from 'fumadocs-core/toc-internal';
|
||||
import * as Primitive from 'fumadocs-core/toc';
|
||||
import {
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
|
||||
@@ -4,7 +4,6 @@ import { defineConfig } from 'tsup';
|
||||
|
||||
const exportedComponents = [
|
||||
'type-table',
|
||||
'roll-button',
|
||||
'image-zoom',
|
||||
'files',
|
||||
'tabs',
|
||||
|
||||
Reference in New Issue
Block a user