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