1import { css } from '@emotion/react'; 2import { spacing, theme } from '@expo/styleguide'; 3import GithubSlugger from 'github-slugger'; 4import Link from 'next/link'; 5import React, { Children, FC, createContext, isValidElement, ReactNode, useContext } from 'react'; 6 7import { TextComponentProps } from './types'; 8 9import { durations } from '~/ui/foundations/durations'; 10 11export const AnchorContext = createContext<GithubSlugger | null>(null); 12 13/** 14 * Render the component with anchor elements and properties. 15 * This adds the following elements: 16 * - hidden link position 17 * - children of the component 18 * - anchor hover icon 19 */ 20export function withAnchor(Component: FC<TextComponentProps>) { 21 function AnchorComponent({ id, children, ...rest }: TextComponentProps) { 22 const slug = useSlug(id, children); 23 return ( 24 <Component css={headingStyle} data-id={slug} {...rest}> 25 <span css={anchorStyle} id={slug} /> 26 <Link href={`#${slug}`} passHref> 27 <a css={linkStyle}>{children}</a> 28 </Link> 29 </Component> 30 ); 31 } 32 AnchorComponent.displayName = `Anchor(${Component.displayName})`; 33 return AnchorComponent; 34} 35 36const headingStyle = css({ 37 position: 'relative', 38}); 39 40const anchorStyle = css({ 41 position: 'relative', 42 top: -100, 43 visibility: 'hidden', 44}); 45 46const linkStyle = css({ 47 position: 'relative', 48 color: 'inherit', 49 textDecoration: 'inherit', 50 51 '::before': { 52 content: '"#"', 53 position: 'absolute', 54 transform: 'translatex(-100%)', 55 transition: `opacity ${durations.hover}`, 56 opacity: 0, 57 color: theme.icon.secondary, 58 padding: `0.25em ${spacing[2]}px`, 59 fontSize: '0.75em', 60 }, 61 62 '&:hover': { 63 '::before': { 64 opacity: 1, 65 }, 66 }, 67}); 68 69function useSlug(id: string | undefined, children: ReactNode) { 70 const slugger = useContext(AnchorContext)!; 71 let slugText = id; 72 73 if (!slugText) { 74 slugText = getTextFromChildren(children); 75 maybeWarnMissingID(slugText); 76 } 77 78 return slugger.slug(slugText); 79} 80 81export function getTextFromChildren(children: ReactNode): string { 82 return Children.toArray(children) 83 .map(child => { 84 if (typeof child === 'string') { 85 return child; 86 } 87 if (isValidElement(child)) { 88 return getTextFromChildren(child.props.children); 89 } 90 return ''; 91 }) 92 .join(' ') 93 .trim(); 94} 95 96/** Eventually, we want to get rid of the auto-generating ID. For now, we need to do this */ 97function maybeWarnMissingID(identifier: ReactNode) { 98 // commenting this out, it's not useful to log this warning, only when actually fixing this issue. 99 // console.warn( 100 // `Anchor element "${identifier}" is missing ID, please add it manually. Auto-generating anchor IDs will be deprecated.` 101 // ); 102} 103