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