Heading.tsx72 lines · main
| 1 | 'use client' |
| 2 | |
| 3 | import { forwardRef, useCallback, type HTMLAttributes } from 'react' |
| 4 | import { useInView } from 'react-intersection-observer' |
| 5 | |
| 6 | import { |
| 7 | getAnchor, |
| 8 | highlightSelectedTocItem, |
| 9 | removeAnchor, |
| 10 | unHighlightSelectedTocItems, |
| 11 | } from './CustomHTMLElements.utils' |
| 12 | |
| 13 | interface 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 | */ |
| 28 | const 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 | ) |
| 70 | Heading.displayName = 'Heading' |
| 71 | |
| 72 | export default Heading |