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