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<TooltipProps> = ({ children, isCode, topOffset }) => ( 97 <div css={[STYLES_TOOLTIP, isCode && STYLES_CODE_TOOLTIP]} style={{ right: 20, top: topOffset }}> 98 {children} 99 </div> 100); 101 102type SidebarLinkProps = { 103 heading: Heading; 104 isActive: boolean; 105 shortenCode: boolean; 106 onClick: (event: React.MouseEvent<HTMLAnchorElement>) => void; 107}; 108 109const DocumentationSidebarRightLink = React.forwardRef<HTMLAnchorElement, SidebarLinkProps>( 110 ({ heading, isActive, shortenCode, onClick }, ref) => { 111 const { slug, level, title, type } = heading; 112 113 const isNested = level <= BASE_HEADING_LEVEL; 114 const isCode = type === HeadingType.InlineCode; 115 116 const paddingLeft = NESTING_OFFSET * (level - BASE_HEADING_LEVEL) + 'px'; 117 const displayTitle = shortenCode && isCode ? trimCodedTitle(title) : title; 118 119 const [tooltipVisible, setTooltipVisible] = React.useState(false); 120 const [tooltipOffset, setTooltipOffset] = React.useState(-20); 121 const onMouseOver = (event: React.MouseEvent<HTMLAnchorElement>) => { 122 setTooltipVisible(isOverflowing(event.currentTarget)); 123 setTooltipOffset(event.currentTarget.getBoundingClientRect().top + 25); 124 }; 125 const onMouseOut = () => { 126 setTooltipVisible(false); 127 }; 128 129 return ( 130 <> 131 {tooltipVisible && ( 132 <Tooltip topOffset={tooltipOffset} isCode={isCode}> 133 {displayTitle} 134 </Tooltip> 135 )} 136 <a 137 ref={ref} 138 onMouseOver={onMouseOver} 139 onMouseOut={onMouseOut} 140 style={{ paddingLeft }} 141 href={'#' + slug} 142 onClick={onClick} 143 css={[ 144 STYLES_LINK, 145 isNested && STYLES_LINK_HEADER, 146 isCode && STYLES_LINK_CODE, 147 isActive && STYLES_LINK_ACTIVE, 148 ]}> 149 {displayTitle} 150 </a> 151 </> 152 ); 153 } 154); 155 156export default DocumentationSidebarRightLink; 157