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