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