Heading.tsx72 lines · main
1'use client'
2
3import { forwardRef, useCallback, type HTMLAttributes } from 'react'
4import { useInView } from 'react-intersection-observer'
5
6import {
7 getAnchor,
8 highlightSelectedTocItem,
9 removeAnchor,
10 unHighlightSelectedTocItems,
11} from './CustomHTMLElements.utils'
12
13interface Props extends HTMLAttributes<HTMLHeadingElement> {
14 tag?: string
15 parseAnchors?: boolean
16 customAnchor?: string
17}
18
19/**
20 * This TOC is used in .mdx files and in .tsx files.
21 * In mdx files, we need to parse the content and format them to match the
22 * expected tocList format (text, link,level).
23 *
24 * In tsx files, we can generate this tocList directly. For these files, we don't
25 * need to parse the <a> and generate anchors. Custom anchors are used in tsx files.
26 * (see: /pages/guides/cli/config.tsx)
27 */
28const Heading = forwardRef(
29 ({ tag, customAnchor, children, ...props }: React.PropsWithChildren<Props>, forwardedRef) => {
30 const HeadingTag = `${tag}` as any
31 const anchor = customAnchor ? customAnchor : getAnchor(children, props)
32 const link = `#${anchor}`
33
34 const { ref: viewRef } = useInView({
35 threshold: 1,
36 rootMargin: '-20% 0% -35% 0px',
37 onChange: (inView, entry) => {
38 if (window.scrollY === 0) unHighlightSelectedTocItems()
39 if (inView) highlightSelectedTocItem(entry.target.id)
40 },
41 })
42
43 const combinedRef = useCallback(
44 (elem: HTMLHeadingElement) => {
45 viewRef(elem)
46 if (typeof forwardedRef === 'function') {
47 forwardedRef(elem)
48 } else if (forwardedRef) {
49 forwardedRef.current = elem
50 }
51 },
52 [forwardedRef, viewRef]
53 )
54
55 return (
56 <HeadingTag id={anchor} ref={combinedRef} className="group scroll-mt-24" {...props}>
57 {removeAnchor(children)}
58 {anchor && (
59 <a
60 href={link}
61 className="ml-2 opacity-0 focus:opacity-100 group-hover:opacity-100 transition"
62 >
63 #
64 </a>
65 )}
66 </HeadingTag>
67 )
68 }
69)
70Heading.displayName = 'Heading'
71
72export default Heading