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