1import { css } from '@emotion/react'; 2import { theme, typography } from '@expo/styleguide'; 3import Link from 'next/link'; 4import * as React from 'react'; 5 6import { BASE_HEADING_LEVEL, Heading, HeadingType } from '~/common/headingManager'; 7import { Tag } from '~/ui/components/Tag'; 8import { MONOSPACE, CALLOUT } from '~/ui/components/Text'; 9 10const STYLES_LINK = css` 11 transition: 50ms ease color; 12 display: flex; 13 text-decoration: none; 14 margin-bottom: 6px; 15 cursor: pointer; 16 overflow: hidden; 17 text-overflow: ellipsis; 18 align-items: center; 19 justify-content: space-between; 20`; 21 22const STYLES_LINK_LABEL = css` 23 overflow: hidden; 24 text-overflow: ellipsis; 25 white-space: nowrap; 26 color: ${theme.text.secondary}; 27 28 :hover { 29 color: ${theme.text.link}; 30 } 31`; 32 33const STYLES_LINK_MONOSPACE = css` 34 ${typography.fontSizes[13]} 35`; 36 37const STYLES_LINK_ACTIVE = css` 38 color: ${theme.text.link}; 39`; 40 41const STYLES_TOOLTIP = css` 42 border-radius: 3px; 43 position: absolute; 44 background-color: ${theme.background.subtle}; 45 max-width: 400px; 46 border: 1px solid ${theme.border.default}; 47 padding: 3px 6px; 48 display: inline-block; 49`; 50 51const STYLES_TOOLTIP_TEXT = css` 52 ${typography.fontSizes[13]} 53 color: ${theme.text.default}; 54 word-break: break-word; 55 word-wrap: normal; 56`; 57 58const STYLES_TOOLTIP_CODE = css` 59 ${typography.fontSizes[12]} 60`; 61 62const STYLES_TAG_CONTAINER = css` 63 display: inline-flex; 64`; 65 66const NESTING_OFFSET = 12; 67 68/** 69 * Replaces `Module.someFunction(arguments: argType)` 70 * with `someFunction()` 71 */ 72const trimCodedTitle = (str: string) => { 73 const dotIdx = str.indexOf('.'); 74 if (dotIdx > 0) str = str.substring(dotIdx + 1); 75 76 const parIdx = str.indexOf('('); 77 if (parIdx > 0) str = str.substring(0, parIdx + 1) + ')'; 78 79 return str; 80}; 81 82/** 83 * Determines if element is overflowing 84 * (its children width exceeds container width) 85 * @param {HTMLElement} el element to check 86 */ 87const isOverflowing = (el: HTMLElement) => { 88 if (!el || !el.children) { 89 return false; 90 } 91 92 const childrenWidth = Array.from(el.children).reduce((sum, child) => sum + child.scrollWidth, 0); 93 const indent = parseInt(window.getComputedStyle(el).paddingLeft, 10); 94 return childrenWidth >= el.scrollWidth - indent; 95}; 96 97type TooltipProps = React.PropsWithChildren<{ 98 isCode?: boolean; 99 topOffset: number; 100}>; 101 102const Tooltip = ({ children, isCode, topOffset }: TooltipProps) => { 103 const ContentWrapper = isCode ? MONOSPACE : CALLOUT; 104 return ( 105 <div css={STYLES_TOOLTIP} style={{ right: 24, top: topOffset }}> 106 <ContentWrapper css={[STYLES_TOOLTIP_TEXT, isCode && STYLES_TOOLTIP_CODE]}> 107 {children} 108 </ContentWrapper> 109 </div> 110 ); 111}; 112 113type SidebarLinkProps = { 114 heading: Heading; 115 isActive: boolean; 116 shortenCode: boolean; 117 onClick: (event: React.MouseEvent<HTMLAnchorElement>) => void; 118}; 119 120const DocumentationSidebarRightLink = React.forwardRef<HTMLAnchorElement, SidebarLinkProps>( 121 ({ heading, isActive, shortenCode, onClick }, ref) => { 122 const { slug, level, title, type, tags } = heading; 123 124 const isCode = type === HeadingType.InlineCode; 125 const paddingLeft = NESTING_OFFSET * (level - BASE_HEADING_LEVEL); 126 const displayTitle = shortenCode && isCode ? trimCodedTitle(title) : title; 127 128 const [tooltipVisible, setTooltipVisible] = React.useState(false); 129 const [tooltipOffset, setTooltipOffset] = React.useState(-20); 130 const onMouseOver = (event: React.MouseEvent<HTMLAnchorElement>) => { 131 setTooltipVisible(isOverflowing(event.currentTarget)); 132 setTooltipOffset( 133 event.currentTarget.getBoundingClientRect().top + event.currentTarget.offsetHeight 134 ); 135 }; 136 137 const onMouseOut = () => { 138 setTooltipVisible(false); 139 }; 140 141 const TitleElement = isCode ? MONOSPACE : CALLOUT; 142 143 return ( 144 <> 145 {tooltipVisible && ( 146 <Tooltip topOffset={tooltipOffset} isCode={isCode}> 147 {displayTitle} 148 </Tooltip> 149 )} 150 <Link 151 ref={ref} 152 onMouseOver={onMouseOver} 153 onMouseOut={onMouseOut} 154 href={'#' + slug} 155 onClick={onClick} 156 css={[STYLES_LINK, paddingLeft && { paddingLeft }]}> 157 <TitleElement 158 css={[ 159 STYLES_LINK_LABEL, 160 isCode && STYLES_LINK_MONOSPACE, 161 isActive && STYLES_LINK_ACTIVE, 162 ]}> 163 {displayTitle} 164 </TitleElement> 165 {tags && tags.length ? ( 166 <div css={STYLES_TAG_CONTAINER}> 167 {tags.map(tag => ( 168 <Tag name={tag} type="toc" key={`${displayTitle}-${tag}`} /> 169 ))} 170 </div> 171 ) : undefined} 172 </Link> 173 </> 174 ); 175 } 176); 177 178export default DocumentationSidebarRightLink; 179