1import { css } from '@emotion/react'; 2import { theme, typography, LinkBase } from '@expo/styleguide'; 3import { spacing } from '@expo/styleguide-base'; 4import { ArrowUpRightIcon } from '@expo/styleguide-icons'; 5import { useRouter } from 'next/compat/router'; 6import type { PropsWithChildren } from 'react'; 7import { useEffect, useRef } from 'react'; 8 9import { stripVersionFromPath } from '~/common/utilities'; 10import { NavigationRoute } from '~/types/common'; 11 12type SidebarLinkProps = PropsWithChildren<{ 13 info: NavigationRoute; 14}>; 15 16const HEAD_NAV_HEIGHT = 160; 17 18const isLinkInViewport = (element: HTMLAnchorElement) => { 19 const rect = element.getBoundingClientRect(); 20 return ( 21 rect.top - HEAD_NAV_HEIGHT >= 0 && 22 rect.left >= 0 && 23 rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && 24 rect.right <= (window.innerWidth || document.documentElement.clientWidth) 25 ); 26}; 27 28export const SidebarLink = ({ info, children }: SidebarLinkProps) => { 29 const router = useRouter(); 30 const ref = useRef<HTMLAnchorElement>(null); 31 32 const checkSelection = () => { 33 // Special case for root url 34 if (info.name === 'Introduction') { 35 if (router?.asPath.match(/\/versions\/[\w.]+\/$/) || router?.asPath === '/versions/latest/') { 36 return true; 37 } 38 } 39 40 const linkUrl = stripVersionFromPath(info.as || info.href); 41 return ( 42 linkUrl === stripVersionFromPath(router?.pathname) || 43 linkUrl === stripVersionFromPath(router?.asPath) 44 ); 45 }; 46 47 const isSelected = checkSelection(); 48 49 useEffect(() => { 50 if (isSelected && ref?.current && !isLinkInViewport(ref?.current)) { 51 setTimeout(() => ref?.current && ref.current.scrollIntoView({ behavior: 'smooth' }), 50); 52 } 53 }, []); 54 55 if (info.hidden) { 56 return null; 57 } 58 59 const customDataAttributes = isSelected && { 60 'data-sidebar-anchor-selected': true, 61 }; 62 const isExternal = info.href.startsWith('http'); 63 64 return ( 65 <div css={STYLES_CONTAINER}> 66 <LinkBase 67 href={info.href as string} 68 ref={ref} 69 css={[STYLES_LINK, isSelected && STYLES_LINK_ACTIVE]} 70 {...customDataAttributes}> 71 <div css={[STYLES_BULLET, isSelected && STYLES_ACTIVE_BULLET]} /> 72 {children} 73 {isExternal && <ArrowUpRightIcon className="icon-sm text-icon-secondary ml-auto" />} 74 </LinkBase> 75 </div> 76 ); 77}; 78 79const STYLES_LINK = css` 80 ${typography.fontSizes[14]} 81 display: flex; 82 flex-direction: row; 83 text-decoration: none; 84 color: ${theme.text.secondary}; 85 transition: 50ms ease color; 86 align-items: center; 87 scroll-margin: 60px; 88 width: 100%; 89 margin-left: -${spacing[2] + spacing[0.5]}px; 90 91 &:hover { 92 color: ${theme.text.link}; 93 } 94 95 &:hover svg { 96 color: ${theme.button.tertiary.icon}; 97 } 98`; 99 100const STYLES_LINK_ACTIVE = css` 101 color: ${theme.text.link}; 102`; 103 104const STYLES_CONTAINER = css` 105 display: flex; 106 min-height: 32px; 107 align-items: center; 108 padding: ${spacing[1]}px; 109 padding-right: ${spacing[2]}px; 110`; 111 112const STYLES_BULLET = css` 113 height: 6px; 114 width: 6px; 115 min-height: 6px; 116 min-width: 6px; 117 border-radius: 100%; 118 margin: ${spacing[2]}px ${spacing[1.5]}px; 119 align-self: self-start; 120`; 121 122const STYLES_ACTIVE_BULLET = css` 123 background-color: ${theme.text.link}; 124`; 125