Core: Change usage of TOC component

This commit is contained in:
fuma-nama
2024-07-25 22:25:19 +08:00
parent 8aebeab316
commit 09c31039c6
9 changed files with 110 additions and 203 deletions
+36
View File
@@ -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 |
+2 -4
View File
@@ -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;
-7
View File
@@ -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"
],
-103
View File
@@ -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
View File
@@ -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>
);
+1 -1
View File
@@ -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 -1
View File
@@ -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,
-1
View File
@@ -4,7 +4,6 @@ import { defineConfig } from 'tsup';
const exportedComponents = [
'type-table',
'roll-button',
'image-zoom',
'files',
'tabs',