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